now fully functional without a steel decimal crate, we are only importing it via cargo, because steel decimal is a separate published crate now
This commit is contained in:
563
Cargo.lock
generated
563
Cargo.lock
generated
@@ -118,18 +118,6 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "anes"
|
|
||||||
version = "0.1.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "anstyle"
|
|
||||||
version = "1.0.11"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anyhow"
|
name = "anyhow"
|
||||||
version = "1.0.98"
|
version = "1.0.98"
|
||||||
@@ -336,21 +324,6 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "bit-set"
|
|
||||||
version = "0.8.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3"
|
|
||||||
dependencies = [
|
|
||||||
"bit-vec",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "bit-vec"
|
|
||||||
version = "0.8.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "2.9.0"
|
version = "2.9.0"
|
||||||
@@ -503,12 +476,6 @@ version = "0.3.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
|
checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cast"
|
|
||||||
version = "0.3.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "castaway"
|
name = "castaway"
|
||||||
version = "0.2.3"
|
version = "0.2.3"
|
||||||
@@ -562,33 +529,6 @@ dependencies = [
|
|||||||
"windows-link",
|
"windows-link",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ciborium"
|
|
||||||
version = "0.2.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e"
|
|
||||||
dependencies = [
|
|
||||||
"ciborium-io",
|
|
||||||
"ciborium-ll",
|
|
||||||
"serde",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ciborium-io"
|
|
||||||
version = "0.2.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ciborium-ll"
|
|
||||||
version = "0.2.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9"
|
|
||||||
dependencies = [
|
|
||||||
"ciborium-io",
|
|
||||||
"half",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cipher"
|
name = "cipher"
|
||||||
version = "0.4.4"
|
version = "0.4.4"
|
||||||
@@ -599,31 +539,6 @@ dependencies = [
|
|||||||
"inout",
|
"inout",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "clap"
|
|
||||||
version = "4.5.40"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "40b6887a1d8685cebccf115538db5c0efe625ccac9696ad45c409d96566e910f"
|
|
||||||
dependencies = [
|
|
||||||
"clap_builder",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "clap_builder"
|
|
||||||
version = "4.5.40"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e0c66c08ce9f0c698cbce5c0279d0bb6ac936d8674174fe48f736533b964f59e"
|
|
||||||
dependencies = [
|
|
||||||
"anstyle",
|
|
||||||
"clap_lex",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "clap_lex"
|
|
||||||
version = "0.7.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "client"
|
name = "client"
|
||||||
version = "0.3.13"
|
version = "0.3.13"
|
||||||
@@ -632,7 +547,7 @@ dependencies = [
|
|||||||
"async-trait",
|
"async-trait",
|
||||||
"common",
|
"common",
|
||||||
"crossterm",
|
"crossterm",
|
||||||
"dirs 6.0.0",
|
"dirs",
|
||||||
"dotenvy",
|
"dotenvy",
|
||||||
"futures",
|
"futures",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
@@ -786,52 +701,6 @@ dependencies = [
|
|||||||
"cfg-if",
|
"cfg-if",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "criterion"
|
|
||||||
version = "0.6.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "3bf7af66b0989381bd0be551bd7cc91912a655a58c6918420c9527b1fd8b4679"
|
|
||||||
dependencies = [
|
|
||||||
"anes",
|
|
||||||
"cast",
|
|
||||||
"ciborium",
|
|
||||||
"clap",
|
|
||||||
"criterion-plot",
|
|
||||||
"itertools 0.13.0",
|
|
||||||
"num-traits",
|
|
||||||
"oorandom",
|
|
||||||
"plotters",
|
|
||||||
"rayon",
|
|
||||||
"regex",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"tinytemplate",
|
|
||||||
"walkdir",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "criterion-plot"
|
|
||||||
version = "0.5.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1"
|
|
||||||
dependencies = [
|
|
||||||
"cast",
|
|
||||||
"itertools 0.10.5",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "crossbeam"
|
|
||||||
version = "0.8.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8"
|
|
||||||
dependencies = [
|
|
||||||
"crossbeam-channel",
|
|
||||||
"crossbeam-deque",
|
|
||||||
"crossbeam-epoch",
|
|
||||||
"crossbeam-queue",
|
|
||||||
"crossbeam-utils",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crossbeam-channel"
|
name = "crossbeam-channel"
|
||||||
version = "0.5.15"
|
version = "0.5.15"
|
||||||
@@ -986,12 +855,6 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "diff"
|
|
||||||
version = "0.1.13"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "digest"
|
name = "digest"
|
||||||
version = "0.10.7"
|
version = "0.10.7"
|
||||||
@@ -1004,34 +867,13 @@ dependencies = [
|
|||||||
"subtle",
|
"subtle",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "dirs"
|
|
||||||
version = "5.0.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225"
|
|
||||||
dependencies = [
|
|
||||||
"dirs-sys 0.4.1",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dirs"
|
name = "dirs"
|
||||||
version = "6.0.0"
|
version = "6.0.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
|
checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"dirs-sys 0.5.0",
|
"dirs-sys",
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "dirs-sys"
|
|
||||||
version = "0.4.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
"option-ext",
|
|
||||||
"redox_users 0.4.6",
|
|
||||||
"windows-sys 0.48.0",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1042,7 +884,7 @@ checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"option-ext",
|
"option-ext",
|
||||||
"redox_users 0.5.0",
|
"redox_users",
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1084,16 +926,6 @@ version = "0.1.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe"
|
checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "env_logger"
|
|
||||||
version = "0.8.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a19187fea3ac7e84da7dacf48de0c45d63c6a76f9490dae389aead16c243fce3"
|
|
||||||
dependencies = [
|
|
||||||
"log",
|
|
||||||
"regex",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "equivalent"
|
name = "equivalent"
|
||||||
version = "1.0.2"
|
version = "1.0.2"
|
||||||
@@ -1405,16 +1237,6 @@ dependencies = [
|
|||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "half"
|
|
||||||
version = "2.6.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9"
|
|
||||||
dependencies = [
|
|
||||||
"cfg-if",
|
|
||||||
"crunchy",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.12.3"
|
version = "0.12.3"
|
||||||
@@ -1793,6 +1615,7 @@ dependencies = [
|
|||||||
"bitmaps",
|
"bitmaps",
|
||||||
"rand_core 0.6.4",
|
"rand_core 0.6.4",
|
||||||
"rand_xoshiro",
|
"rand_xoshiro",
|
||||||
|
"serde",
|
||||||
"sized-chunks",
|
"sized-chunks",
|
||||||
"typenum",
|
"typenum",
|
||||||
"version_check",
|
"version_check",
|
||||||
@@ -1816,6 +1639,7 @@ dependencies = [
|
|||||||
"bitmaps",
|
"bitmaps",
|
||||||
"rand_core 0.6.4",
|
"rand_core 0.6.4",
|
||||||
"rand_xoshiro",
|
"rand_xoshiro",
|
||||||
|
"serde",
|
||||||
"sized-chunks",
|
"sized-chunks",
|
||||||
"typenum",
|
"typenum",
|
||||||
"version_check",
|
"version_check",
|
||||||
@@ -1869,15 +1693,6 @@ dependencies = [
|
|||||||
"syn 2.0.100",
|
"syn 2.0.100",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "itertools"
|
|
||||||
version = "0.10.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
|
|
||||||
dependencies = [
|
|
||||||
"either",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itertools"
|
name = "itertools"
|
||||||
version = "0.13.0"
|
version = "0.13.0"
|
||||||
@@ -2177,20 +1992,6 @@ dependencies = [
|
|||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "num"
|
|
||||||
version = "0.4.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23"
|
|
||||||
dependencies = [
|
|
||||||
"num-bigint",
|
|
||||||
"num-complex",
|
|
||||||
"num-integer",
|
|
||||||
"num-iter",
|
|
||||||
"num-rational",
|
|
||||||
"num-traits",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-bigint"
|
name = "num-bigint"
|
||||||
version = "0.4.6"
|
version = "0.4.6"
|
||||||
@@ -2219,16 +2020,6 @@ dependencies = [
|
|||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "num-complex"
|
|
||||||
version = "0.4.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495"
|
|
||||||
dependencies = [
|
|
||||||
"num-traits",
|
|
||||||
"serde",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-conv"
|
name = "num-conv"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -2264,7 +2055,6 @@ dependencies = [
|
|||||||
"num-bigint",
|
"num-bigint",
|
||||||
"num-integer",
|
"num-integer",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
"serde",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2307,12 +2097,6 @@ version = "0.1.11"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b4ce411919553d3f9fa53a0880544cda985a112117a0444d5ff1e870a893d6ea"
|
checksum = "b4ce411919553d3f9fa53a0880544cda985a112117a0444d5ff1e870a893d6ea"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "oorandom"
|
|
||||||
version = "11.1.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openssl"
|
name = "openssl"
|
||||||
version = "0.10.72"
|
version = "0.10.72"
|
||||||
@@ -2507,34 +2291,6 @@ version = "0.3.32"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
|
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "plotters"
|
|
||||||
version = "0.3.7"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747"
|
|
||||||
dependencies = [
|
|
||||||
"num-traits",
|
|
||||||
"plotters-backend",
|
|
||||||
"plotters-svg",
|
|
||||||
"wasm-bindgen",
|
|
||||||
"web-sys",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "plotters-backend"
|
|
||||||
version = "0.3.7"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "plotters-svg"
|
|
||||||
version = "0.3.7"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670"
|
|
||||||
dependencies = [
|
|
||||||
"plotters-backend",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "polling"
|
name = "polling"
|
||||||
version = "3.7.4"
|
version = "3.7.4"
|
||||||
@@ -2576,16 +2332,6 @@ dependencies = [
|
|||||||
"unicode-width 0.1.14",
|
"unicode-width 0.1.14",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pretty_assertions"
|
|
||||||
version = "1.4.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d"
|
|
||||||
dependencies = [
|
|
||||||
"diff",
|
|
||||||
"yansi",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "prettyplease"
|
name = "prettyplease"
|
||||||
version = "0.2.32"
|
version = "0.2.32"
|
||||||
@@ -2636,26 +2382,6 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "proptest"
|
|
||||||
version = "1.7.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "6fcdab19deb5195a31cf7726a210015ff1496ba1464fd42cb4f537b8b01b471f"
|
|
||||||
dependencies = [
|
|
||||||
"bit-set",
|
|
||||||
"bit-vec",
|
|
||||||
"bitflags",
|
|
||||||
"lazy_static",
|
|
||||||
"num-traits",
|
|
||||||
"rand 0.9.1",
|
|
||||||
"rand_chacha 0.9.0",
|
|
||||||
"rand_xorshift",
|
|
||||||
"regex-syntax",
|
|
||||||
"rusty-fork",
|
|
||||||
"tempfile",
|
|
||||||
"unarray",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "prost"
|
name = "prost"
|
||||||
version = "0.13.5"
|
version = "0.13.5"
|
||||||
@@ -2728,23 +2454,6 @@ dependencies = [
|
|||||||
"syn 1.0.109",
|
"syn 1.0.109",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "quick-error"
|
|
||||||
version = "1.2.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "quickcheck"
|
|
||||||
version = "1.0.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "588f6378e4dd99458b60ec275b4477add41ce4fa9f64dcba6f15adccb19b50d6"
|
|
||||||
dependencies = [
|
|
||||||
"env_logger",
|
|
||||||
"log",
|
|
||||||
"rand 0.8.5",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quickscope"
|
name = "quickscope"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
@@ -2776,12 +2485,6 @@ version = "0.7.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
|
checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "radix_fmt"
|
|
||||||
version = "1.0.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ce082a9940a7ace2ad4a8b7d0b1eac6aa378895f18be598230c5f2284ac05426"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rand"
|
name = "rand"
|
||||||
version = "0.8.5"
|
version = "0.8.5"
|
||||||
@@ -2851,15 +2554,6 @@ dependencies = [
|
|||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rand_xorshift"
|
|
||||||
version = "0.4.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a"
|
|
||||||
dependencies = [
|
|
||||||
"rand_core 0.9.3",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rand_xoshiro"
|
name = "rand_xoshiro"
|
||||||
version = "0.6.0"
|
version = "0.6.0"
|
||||||
@@ -2919,17 +2613,6 @@ dependencies = [
|
|||||||
"bitflags",
|
"bitflags",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "redox_users"
|
|
||||||
version = "0.4.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43"
|
|
||||||
dependencies = [
|
|
||||||
"getrandom 0.2.15",
|
|
||||||
"libredox",
|
|
||||||
"thiserror 1.0.69",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "redox_users"
|
name = "redox_users"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
@@ -2938,7 +2621,7 @@ checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"getrandom 0.2.15",
|
"getrandom 0.2.15",
|
||||||
"libredox",
|
"libredox",
|
||||||
"thiserror 2.0.12",
|
"thiserror",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3176,42 +2859,12 @@ version = "1.0.20"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2"
|
checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rusty-fork"
|
|
||||||
version = "0.3.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f"
|
|
||||||
dependencies = [
|
|
||||||
"fnv",
|
|
||||||
"quick-error",
|
|
||||||
"tempfile",
|
|
||||||
"wait-timeout",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ryu"
|
name = "ryu"
|
||||||
version = "1.0.20"
|
version = "1.0.20"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
|
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "same-file"
|
|
||||||
version = "1.0.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
|
|
||||||
dependencies = [
|
|
||||||
"winapi-util",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "scc"
|
|
||||||
version = "2.3.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "22b2d775fb28f245817589471dd49c5edf64237f4a19d10ce9a92ff4651a27f4"
|
|
||||||
dependencies = [
|
|
||||||
"sdd",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "schannel"
|
name = "schannel"
|
||||||
version = "0.1.27"
|
version = "0.1.27"
|
||||||
@@ -3227,12 +2880,6 @@ version = "1.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "sdd"
|
|
||||||
version = "3.0.8"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "584e070911c7017da6cb2eb0788d09f43d789029b5877d3e5ecc8acf86ceee21"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "seahash"
|
name = "seahash"
|
||||||
version = "4.1.0"
|
version = "4.1.0"
|
||||||
@@ -3338,31 +2985,6 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "serial_test"
|
|
||||||
version = "3.2.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1b258109f244e1d6891bf1053a55d63a5cd4f8f4c30cf9a1280989f80e7a1fa9"
|
|
||||||
dependencies = [
|
|
||||||
"futures",
|
|
||||||
"log",
|
|
||||||
"once_cell",
|
|
||||||
"parking_lot",
|
|
||||||
"scc",
|
|
||||||
"serial_test_derive",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "serial_test_derive"
|
|
||||||
version = "3.2.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "5d69265a08751de7844521fd15003ae0a888e035773ba05695c5c759a6f89eef"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn 2.0.100",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "server"
|
name = "server"
|
||||||
version = "0.3.13"
|
version = "0.3.13"
|
||||||
@@ -3389,10 +3011,10 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
"steel-core",
|
"steel-core",
|
||||||
"steel-derive 0.5.0 (git+https://github.com/mattwparas/steel.git?branch=master)",
|
"steel-decimal",
|
||||||
"steel_decimal",
|
"steel-derive",
|
||||||
"tantivy",
|
"tantivy",
|
||||||
"thiserror 2.0.12",
|
"thiserror",
|
||||||
"time",
|
"time",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tonic",
|
"tonic",
|
||||||
@@ -3493,7 +3115,7 @@ checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"num-bigint",
|
"num-bigint",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
"thiserror 2.0.12",
|
"thiserror",
|
||||||
"time",
|
"time",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -3606,7 +3228,7 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
"sha2",
|
"sha2",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"thiserror 2.0.12",
|
"thiserror",
|
||||||
"time",
|
"time",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-stream",
|
"tokio-stream",
|
||||||
@@ -3692,7 +3314,7 @@ dependencies = [
|
|||||||
"smallvec",
|
"smallvec",
|
||||||
"sqlx-core",
|
"sqlx-core",
|
||||||
"stringprep",
|
"stringprep",
|
||||||
"thiserror 2.0.12",
|
"thiserror",
|
||||||
"time",
|
"time",
|
||||||
"tracing",
|
"tracing",
|
||||||
"uuid",
|
"uuid",
|
||||||
@@ -3733,7 +3355,7 @@ dependencies = [
|
|||||||
"smallvec",
|
"smallvec",
|
||||||
"sqlx-core",
|
"sqlx-core",
|
||||||
"stringprep",
|
"stringprep",
|
||||||
"thiserror 2.0.12",
|
"thiserror",
|
||||||
"time",
|
"time",
|
||||||
"tracing",
|
"tracing",
|
||||||
"uuid",
|
"uuid",
|
||||||
@@ -3760,7 +3382,7 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"serde_urlencoded",
|
"serde_urlencoded",
|
||||||
"sqlx-core",
|
"sqlx-core",
|
||||||
"thiserror 2.0.12",
|
"thiserror",
|
||||||
"time",
|
"time",
|
||||||
"tracing",
|
"tracing",
|
||||||
"url",
|
"url",
|
||||||
@@ -3781,8 +3403,9 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "steel-core"
|
name = "steel-core"
|
||||||
version = "0.6.0"
|
version = "0.7.0"
|
||||||
source = "git+https://github.com/mattwparas/steel.git#2f28ab10523198726d343257d29d892864e897b0"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "79fcd4a04c6b2fc5f695ad37b379ad0e4245a9a12dcd742fe4fe1ad15ddd6546"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"abi_stable",
|
"abi_stable",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
@@ -3793,55 +3416,62 @@ dependencies = [
|
|||||||
"chrono",
|
"chrono",
|
||||||
"codespan-reporting",
|
"codespan-reporting",
|
||||||
"compact_str",
|
"compact_str",
|
||||||
"crossbeam",
|
"crossbeam-channel",
|
||||||
"dirs 5.0.1",
|
"crossbeam-utils",
|
||||||
|
"env_home",
|
||||||
"futures-executor",
|
"futures-executor",
|
||||||
"futures-task",
|
"futures-task",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"fxhash",
|
"fxhash",
|
||||||
"getrandom 0.3.2",
|
"getrandom 0.3.2",
|
||||||
"home",
|
"glob",
|
||||||
"http",
|
|
||||||
"httparse",
|
"httparse",
|
||||||
"im",
|
"im",
|
||||||
"im-lists",
|
"im-lists",
|
||||||
"im-rc",
|
"im-rc",
|
||||||
"lasso",
|
"lasso",
|
||||||
"log",
|
"log",
|
||||||
"num",
|
"md-5",
|
||||||
|
"num-bigint",
|
||||||
|
"num-integer",
|
||||||
|
"num-rational",
|
||||||
|
"num-traits",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"polling",
|
"polling",
|
||||||
"pretty",
|
|
||||||
"quickscope",
|
"quickscope",
|
||||||
"radix_fmt",
|
|
||||||
"rand 0.9.1",
|
"rand 0.9.1",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_derive",
|
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"steel-derive 0.5.0 (git+https://github.com/mattwparas/steel.git)",
|
"steel-derive",
|
||||||
"steel-gen",
|
"steel-gen",
|
||||||
"steel-parser",
|
"steel-parser",
|
||||||
"strsim",
|
"strsim",
|
||||||
"weak-table",
|
"weak-table",
|
||||||
"which",
|
"which",
|
||||||
|
"xdg",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "steel-derive"
|
name = "steel-decimal"
|
||||||
version = "0.5.0"
|
version = "1.0.0"
|
||||||
source = "git+https://github.com/mattwparas/steel.git?branch=master#2f28ab10523198726d343257d29d892864e897b0"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c43950a3eed43f3e9765a51f5dc1b0de5e1687ba824b8589990747d9ba241187"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"regex",
|
||||||
"quote",
|
"rust_decimal",
|
||||||
"syn 2.0.100",
|
"rust_decimal_macros",
|
||||||
|
"steel-core",
|
||||||
|
"steel-derive",
|
||||||
|
"thiserror",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "steel-derive"
|
name = "steel-derive"
|
||||||
version = "0.5.0"
|
version = "0.6.0"
|
||||||
source = "git+https://github.com/mattwparas/steel.git#2f28ab10523198726d343257d29d892864e897b0"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bab7d1c6e3ee7b3ba6a4187b545d7dc1fa6e4929f0721a8798cfc3b8c7ed2b23"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -3850,50 +3480,32 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "steel-gen"
|
name = "steel-gen"
|
||||||
version = "0.2.0"
|
version = "0.3.0"
|
||||||
source = "git+https://github.com/mattwparas/steel.git#2f28ab10523198726d343257d29d892864e897b0"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "deabc06d4f4735677503f593ae185d3f47c0fa8089b7e9a7177e0e0544483d86"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"codegen",
|
"codegen",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_derive",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "steel-parser"
|
name = "steel-parser"
|
||||||
version = "0.6.0"
|
version = "0.7.0"
|
||||||
source = "git+https://github.com/mattwparas/steel.git#2f28ab10523198726d343257d29d892864e897b0"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7cf95d03e34e5d34a318a1f3ad7c933628c476776d3dc4fe9accb5b7b6b5a295"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"compact_str",
|
"compact_str",
|
||||||
"fxhash",
|
"fxhash",
|
||||||
"lasso",
|
"lasso",
|
||||||
"num",
|
"num-bigint",
|
||||||
|
"num-rational",
|
||||||
|
"num-traits",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"pretty",
|
"pretty",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_derive",
|
|
||||||
"smallvec",
|
"smallvec",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "steel_decimal"
|
|
||||||
version = "1.0.0"
|
|
||||||
dependencies = [
|
|
||||||
"criterion",
|
|
||||||
"pretty_assertions",
|
|
||||||
"proptest",
|
|
||||||
"quickcheck",
|
|
||||||
"regex",
|
|
||||||
"rstest",
|
|
||||||
"rust_decimal",
|
|
||||||
"rust_decimal_macros",
|
|
||||||
"serial_test",
|
|
||||||
"steel-core",
|
|
||||||
"steel-derive 0.5.0 (git+https://github.com/mattwparas/steel.git?branch=master)",
|
|
||||||
"tempfile",
|
|
||||||
"thiserror 2.0.12",
|
|
||||||
"tokio-test",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "stringprep"
|
name = "stringprep"
|
||||||
version = "0.1.5"
|
version = "0.1.5"
|
||||||
@@ -4024,7 +3636,7 @@ dependencies = [
|
|||||||
"tantivy-stacker",
|
"tantivy-stacker",
|
||||||
"tantivy-tokenizer-api",
|
"tantivy-tokenizer-api",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"thiserror 2.0.12",
|
"thiserror",
|
||||||
"time",
|
"time",
|
||||||
"uuid",
|
"uuid",
|
||||||
"winapi",
|
"winapi",
|
||||||
@@ -4152,33 +3764,13 @@ dependencies = [
|
|||||||
"winapi-util",
|
"winapi-util",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "thiserror"
|
|
||||||
version = "1.0.69"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
|
|
||||||
dependencies = [
|
|
||||||
"thiserror-impl 1.0.69",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "2.0.12"
|
version = "2.0.12"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
|
checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"thiserror-impl 2.0.12",
|
"thiserror-impl",
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "thiserror-impl"
|
|
||||||
version = "1.0.69"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn 2.0.100",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4245,16 +3837,6 @@ dependencies = [
|
|||||||
"zerovec",
|
"zerovec",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tinytemplate"
|
|
||||||
version = "1.2.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc"
|
|
||||||
dependencies = [
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tinyvec"
|
name = "tinyvec"
|
||||||
version = "1.9.0"
|
version = "1.9.0"
|
||||||
@@ -4560,12 +4142,6 @@ version = "1.18.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f"
|
checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "unarray"
|
|
||||||
version = "0.1.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-bidi"
|
name = "unicode-bidi"
|
||||||
version = "0.3.18"
|
version = "0.3.18"
|
||||||
@@ -4717,25 +4293,6 @@ version = "0.9.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "wait-timeout"
|
|
||||||
version = "0.2.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "walkdir"
|
|
||||||
version = "2.5.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
|
|
||||||
dependencies = [
|
|
||||||
"same-file",
|
|
||||||
"winapi-util",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "want"
|
name = "want"
|
||||||
version = "0.3.1"
|
version = "0.3.1"
|
||||||
@@ -4830,16 +4387,6 @@ version = "0.3.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "323f4da9523e9a669e1eaf9c6e763892769b1d38c623913647bfdc1532fe4549"
|
checksum = "323f4da9523e9a669e1eaf9c6e763892769b1d38c623913647bfdc1532fe4549"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "web-sys"
|
|
||||||
version = "0.3.77"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2"
|
|
||||||
dependencies = [
|
|
||||||
"js-sys",
|
|
||||||
"wasm-bindgen",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "which"
|
name = "which"
|
||||||
version = "7.0.3"
|
version = "7.0.3"
|
||||||
@@ -5146,10 +4693,10 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "yansi"
|
name = "xdg"
|
||||||
version = "1.0.1"
|
version = "3.0.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
|
checksum = "2fb433233f2df9344722454bc7e96465c9d03bff9d77c248f9e7523fe79585b5"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "yoke"
|
name = "yoke"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
members = ["client", "server", "common", "search", "steel_decimal"]
|
members = ["client", "server", "common", "search"]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ license = "AGPL-3.0-or-later"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
common = { path = "../common" }
|
common = { path = "../common" }
|
||||||
search = { path = "../search" }
|
search = { path = "../search" }
|
||||||
steel_decimal = { path = "../steel_decimal" }
|
|
||||||
|
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
tantivy = { workspace = true }
|
tantivy = { workspace = true }
|
||||||
@@ -23,8 +22,10 @@ tonic = "0.13.0"
|
|||||||
tonic-reflection = "0.13.0"
|
tonic-reflection = "0.13.0"
|
||||||
tracing = "0.1.41"
|
tracing = "0.1.41"
|
||||||
time = { version = "0.3.41", features = ["local-offset"] }
|
time = { version = "0.3.41", features = ["local-offset"] }
|
||||||
steel-derive = { git = "https://github.com/mattwparas/steel.git", branch = "master", package = "steel-derive" }
|
|
||||||
steel-core = { git = "https://github.com/mattwparas/steel.git", version = "0.6.0", features = ["anyhow", "dylibs", "sync"] }
|
steel-derive = "0.6.0"
|
||||||
|
steel-core = { version = "0.7.0", features = ["anyhow", "dylibs", "sync"] }
|
||||||
|
|
||||||
dashmap = "6.1.0"
|
dashmap = "6.1.0"
|
||||||
lazy_static = "1.5.0"
|
lazy_static = "1.5.0"
|
||||||
bcrypt = "0.17.0"
|
bcrypt = "0.17.0"
|
||||||
@@ -37,6 +38,7 @@ rust_decimal = { workspace = true }
|
|||||||
rust_decimal_macros = { workspace = true }
|
rust_decimal_macros = { workspace = true }
|
||||||
regex = { workspace = true }
|
regex = { workspace = true }
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
|
steel-decimal = "1.0.0"
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
name = "server"
|
name = "server"
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
# Changelog
|
|
||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
|
||||||
|
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
||||||
|
|
||||||
|
|
||||||
## [1.0.0] - 2025-07-07
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- Initial release of steel_decimal crate
|
|
||||||
- High-precision decimal arithmetic for Steel programming language
|
|
||||||
- Automatic script transformation from standard math to decimal operations
|
|
||||||
- Comprehensive mathematical functions:
|
|
||||||
- Basic arithmetic (add, subtract, multiply, divide)
|
|
||||||
- Advanced mathematics (power, square root, logarithms, exponential)
|
|
||||||
- Trigonometric functions (sin, cos, tan)
|
|
||||||
- Comparison operations
|
|
||||||
- Utility functions (abs, min, max, round)
|
|
||||||
- Financial calculation functions
|
|
||||||
- Precision control with thread-local contexts
|
|
||||||
- Variable support with validation
|
|
||||||
- Selective function registration via builder pattern
|
|
||||||
- Scientific notation support in decimal parsing
|
|
||||||
- Comprehensive test suite with 100% pass rate
|
|
||||||
- Property-based testing with proptest
|
|
||||||
- Concurrency testing
|
|
||||||
- Security and boundary testing
|
|
||||||
|
|
||||||
### Features
|
|
||||||
- Thread-safe precision contexts
|
|
||||||
- Configurable function registration
|
|
||||||
- Variable interpolation in scripts
|
|
||||||
- Script validation and dependency extraction
|
|
||||||
- Type conversion utilities
|
|
||||||
- Error handling with detailed messages
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "steel_decimal"
|
|
||||||
version = "1.0.0"
|
|
||||||
edition.workspace = true
|
|
||||||
license = "MIT OR Apache-2.0"
|
|
||||||
authors.workspace = true
|
|
||||||
description = "High-precision decimal arithmetic for Steel programming language"
|
|
||||||
readme = "README.md"
|
|
||||||
categories = ["science", "financial", "scripting"]
|
|
||||||
keywords = ["decimal", "steel", "accounting", "precision"]
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
steel-derive = { git = "https://github.com/mattwparas/steel.git", branch = "master", package = "steel-derive" }
|
|
||||||
steel-core = { git = "https://github.com/mattwparas/steel.git", version = "0.6.0", features = ["anyhow", "dylibs", "sync"] }
|
|
||||||
|
|
||||||
rust_decimal = { workspace = true }
|
|
||||||
rust_decimal_macros = { workspace = true }
|
|
||||||
regex = { workspace = true }
|
|
||||||
thiserror = { workspace = true }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
rstest = "0.25.0"
|
|
||||||
tokio-test = "0.4.4"
|
|
||||||
serial_test = "3.2.0"
|
|
||||||
pretty_assertions = "1.4.1"
|
|
||||||
tempfile = "3.20.0"
|
|
||||||
proptest = "1.7.0"
|
|
||||||
quickcheck = "1.0.3"
|
|
||||||
criterion = { version = "0.6.0", features = ["html_reports"] }
|
|
||||||
|
|
||||||
@@ -1,202 +0,0 @@
|
|||||||
|
|
||||||
Apache License
|
|
||||||
Version 2.0, January 2004
|
|
||||||
http://www.apache.org/licenses/
|
|
||||||
|
|
||||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
||||||
|
|
||||||
1. Definitions.
|
|
||||||
|
|
||||||
"License" shall mean the terms and conditions for use, reproduction,
|
|
||||||
and distribution as defined by Sections 1 through 9 of this document.
|
|
||||||
|
|
||||||
"Licensor" shall mean the copyright owner or entity authorized by
|
|
||||||
the copyright owner that is granting the License.
|
|
||||||
|
|
||||||
"Legal Entity" shall mean the union of the acting entity and all
|
|
||||||
other entities that control, are controlled by, or are under common
|
|
||||||
control with that entity. For the purposes of this definition,
|
|
||||||
"control" means (i) the power, direct or indirect, to cause the
|
|
||||||
direction or management of such entity, whether by contract or
|
|
||||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
||||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
||||||
|
|
||||||
"You" (or "Your") shall mean an individual or Legal Entity
|
|
||||||
exercising permissions granted by this License.
|
|
||||||
|
|
||||||
"Source" form shall mean the preferred form for making modifications,
|
|
||||||
including but not limited to software source code, documentation
|
|
||||||
source, and configuration files.
|
|
||||||
|
|
||||||
"Object" form shall mean any form resulting from mechanical
|
|
||||||
transformation or translation of a Source form, including but
|
|
||||||
not limited to compiled object code, generated documentation,
|
|
||||||
and conversions to other media types.
|
|
||||||
|
|
||||||
"Work" shall mean the work of authorship, whether in Source or
|
|
||||||
Object form, made available under the License, as indicated by a
|
|
||||||
copyright notice that is included in or attached to the work
|
|
||||||
(an example is provided in the Appendix below).
|
|
||||||
|
|
||||||
"Derivative Works" shall mean any work, whether in Source or Object
|
|
||||||
form, that is based on (or derived from) the Work and for which the
|
|
||||||
editorial revisions, annotations, elaborations, or other modifications
|
|
||||||
represent, as a whole, an original work of authorship. For the purposes
|
|
||||||
of this License, Derivative Works shall not include works that remain
|
|
||||||
separable from, or merely link (or bind by name) to the interfaces of,
|
|
||||||
the Work and Derivative Works thereof.
|
|
||||||
|
|
||||||
"Contribution" shall mean any work of authorship, including
|
|
||||||
the original version of the Work and any modifications or additions
|
|
||||||
to that Work or Derivative Works thereof, that is intentionally
|
|
||||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
||||||
or by an individual or Legal Entity authorized to submit on behalf of
|
|
||||||
the copyright owner. For the purposes of this definition, "submitted"
|
|
||||||
means any form of electronic, verbal, or written communication sent
|
|
||||||
to the Licensor or its representatives, including but not limited to
|
|
||||||
communication on electronic mailing lists, source code control systems,
|
|
||||||
and issue tracking systems that are managed by, or on behalf of, the
|
|
||||||
Licensor for the purpose of discussing and improving the Work, but
|
|
||||||
excluding communication that is conspicuously marked or otherwise
|
|
||||||
designated in writing by the copyright owner as "Not a Contribution."
|
|
||||||
|
|
||||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
||||||
on behalf of whom a Contribution has been received by Licensor and
|
|
||||||
subsequently incorporated within the Work.
|
|
||||||
|
|
||||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
||||||
this License, each Contributor hereby grants to You a perpetual,
|
|
||||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
||||||
copyright license to reproduce, prepare Derivative Works of,
|
|
||||||
publicly display, publicly perform, sublicense, and distribute the
|
|
||||||
Work and such Derivative Works in Source or Object form.
|
|
||||||
|
|
||||||
3. Grant of Patent License. Subject to the terms and conditions of
|
|
||||||
this License, each Contributor hereby grants to You a perpetual,
|
|
||||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
||||||
(except as stated in this section) patent license to make, have made,
|
|
||||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
||||||
where such license applies only to those patent claims licensable
|
|
||||||
by such Contributor that are necessarily infringed by their
|
|
||||||
Contribution(s) alone or by combination of their Contribution(s)
|
|
||||||
with the Work to which such Contribution(s) was submitted. If You
|
|
||||||
institute patent litigation against any entity (including a
|
|
||||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
||||||
or a Contribution incorporated within the Work constitutes direct
|
|
||||||
or contributory patent infringement, then any patent licenses
|
|
||||||
granted to You under this License for that Work shall terminate
|
|
||||||
as of the date such litigation is filed.
|
|
||||||
|
|
||||||
4. Redistribution. You may reproduce and distribute copies of the
|
|
||||||
Work or Derivative Works thereof in any medium, with or without
|
|
||||||
modifications, and in Source or Object form, provided that You
|
|
||||||
meet the following conditions:
|
|
||||||
|
|
||||||
(a) You must give any other recipients of the Work or
|
|
||||||
Derivative Works a copy of this License; and
|
|
||||||
|
|
||||||
(b) You must cause any modified files to carry prominent notices
|
|
||||||
stating that You changed the files; and
|
|
||||||
|
|
||||||
(c) You must retain, in the Source form of any Derivative Works
|
|
||||||
that You distribute, all copyright, patent, trademark, and
|
|
||||||
attribution notices from the Source form of the Work,
|
|
||||||
excluding those notices that do not pertain to any part of
|
|
||||||
the Derivative Works; and
|
|
||||||
|
|
||||||
(d) If the Work includes a "NOTICE" text file as part of its
|
|
||||||
distribution, then any Derivative Works that You distribute must
|
|
||||||
include a readable copy of the attribution notices contained
|
|
||||||
within such NOTICE file, excluding those notices that do not
|
|
||||||
pertain to any part of the Derivative Works, in at least one
|
|
||||||
of the following places: within a NOTICE text file distributed
|
|
||||||
as part of the Derivative Works; within the Source form or
|
|
||||||
documentation, if provided along with the Derivative Works; or,
|
|
||||||
within a display generated by the Derivative Works, if and
|
|
||||||
wherever such third-party notices normally appear. The contents
|
|
||||||
of the NOTICE file are for informational purposes only and
|
|
||||||
do not modify the License. You may add Your own attribution
|
|
||||||
notices within Derivative Works that You distribute, alongside
|
|
||||||
or as an addendum to the NOTICE text from the Work, provided
|
|
||||||
that such additional attribution notices cannot be construed
|
|
||||||
as modifying the License.
|
|
||||||
|
|
||||||
You may add Your own copyright statement to Your modifications and
|
|
||||||
may provide additional or different license terms and conditions
|
|
||||||
for use, reproduction, or distribution of Your modifications, or
|
|
||||||
for any such Derivative Works as a whole, provided Your use,
|
|
||||||
reproduction, and distribution of the Work otherwise complies with
|
|
||||||
the conditions stated in this License.
|
|
||||||
|
|
||||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
||||||
any Contribution intentionally submitted for inclusion in the Work
|
|
||||||
by You to the Licensor shall be under the terms and conditions of
|
|
||||||
this License, without any additional terms or conditions.
|
|
||||||
Notwithstanding the above, nothing herein shall supersede or modify
|
|
||||||
the terms of any separate license agreement you may have executed
|
|
||||||
with Licensor regarding such Contributions.
|
|
||||||
|
|
||||||
6. Trademarks. This License does not grant permission to use the trade
|
|
||||||
names, trademarks, service marks, or product names of the Licensor,
|
|
||||||
except as required for reasonable and customary use in describing the
|
|
||||||
origin of the Work and reproducing the content of the NOTICE file.
|
|
||||||
|
|
||||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
||||||
agreed to in writing, Licensor provides the Work (and each
|
|
||||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
||||||
implied, including, without limitation, any warranties or conditions
|
|
||||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
||||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
||||||
appropriateness of using or redistributing the Work and assume any
|
|
||||||
risks associated with Your exercise of permissions under this License.
|
|
||||||
|
|
||||||
8. Limitation of Liability. In no event and under no legal theory,
|
|
||||||
whether in tort (including negligence), contract, or otherwise,
|
|
||||||
unless required by applicable law (such as deliberate and grossly
|
|
||||||
negligent acts) or agreed to in writing, shall any Contributor be
|
|
||||||
liable to You for damages, including any direct, indirect, special,
|
|
||||||
incidental, or consequential damages of any character arising as a
|
|
||||||
result of this License or out of the use or inability to use the
|
|
||||||
Work (including but not limited to damages for loss of goodwill,
|
|
||||||
work stoppage, computer failure or malfunction, or any and all
|
|
||||||
other commercial damages or losses), even if such Contributor
|
|
||||||
has been advised of the possibility of such damages.
|
|
||||||
|
|
||||||
9. Accepting Warranty or Additional Liability. While redistributing
|
|
||||||
the Work or Derivative Works thereof, You may choose to offer,
|
|
||||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
||||||
or other liability obligations and/or rights consistent with this
|
|
||||||
License. However, in accepting such obligations, You may act only
|
|
||||||
on Your own behalf and on Your sole responsibility, not on behalf
|
|
||||||
of any other Contributor, and only if You agree to indemnify,
|
|
||||||
defend, and hold each Contributor harmless for any liability
|
|
||||||
incurred by, or claims asserted against, such Contributor by reason
|
|
||||||
of your accepting any such warranty or additional liability.
|
|
||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
|
||||||
|
|
||||||
APPENDIX: How to apply the Apache License to your work.
|
|
||||||
|
|
||||||
To apply the Apache License to your work, attach the following
|
|
||||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
||||||
replaced with your own identifying information. (Don't include
|
|
||||||
the brackets!) The text should be enclosed in the appropriate
|
|
||||||
comment syntax for the file format. We also recommend that a
|
|
||||||
file or class name and description of purpose be included on the
|
|
||||||
same "printed page" as the copyright notice for easier
|
|
||||||
identification within third-party archives.
|
|
||||||
|
|
||||||
Copyright 2025 Filip Priečinský
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
Copyright 2025-07-07 Filip Priečinský
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
# Simple Makefile for Steel Decimal
|
|
||||||
|
|
||||||
# Production test settings
|
|
||||||
export PROPTEST_CASES=10000
|
|
||||||
export RUST_BACKTRACE=1
|
|
||||||
|
|
||||||
.PHONY: test check
|
|
||||||
|
|
||||||
# Run all tests with production settings
|
|
||||||
test:
|
|
||||||
@echo "Running all tests..."
|
|
||||||
@cargo test --release
|
|
||||||
|
|
||||||
# Quick development test
|
|
||||||
test-dev:
|
|
||||||
@PROPTEST_CASES=100 cargo test
|
|
||||||
|
|
||||||
# Check compilation
|
|
||||||
check:
|
|
||||||
@cargo check
|
|
||||||
|
|
||||||
# Clean build artifacts
|
|
||||||
clean:
|
|
||||||
@cargo clean
|
|
||||||
|
|
||||||
# Run with specific test threads for concurrency tests
|
|
||||||
test-concurrent:
|
|
||||||
@cargo test concurrency_tests --release -- --test-threads=1
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
# Steel Decimal
|
|
||||||
|
|
||||||
A Rust library that provides high-precision decimal arithmetic support for the [Steel programming language](https://github.com/mattwparas/steel). This crate transforms Steel scripts to use decimal operations and provides function registration utilities for Steel VMs.
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
- **High-precision decimal arithmetic** using `rust_decimal` crate
|
|
||||||
- **Automatic script transformation** from standard math to decimal operations
|
|
||||||
- **Comprehensive mathematical functions** (basic arithmetic, trigonometry, logarithms)
|
|
||||||
- **Financial calculations** with proper decimal precision
|
|
||||||
- **Configurable precision control**
|
|
||||||
- **Selective function registration** via builder pattern
|
|
||||||
- **Variable support** with validation
|
|
||||||
- **Thread-safe** precision contexts
|
|
||||||
|
|
||||||
## Quick Start
|
|
||||||
|
|
||||||
Add this to your `Cargo.toml`:
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[dependencies]
|
|
||||||
steel_decimal = "1.0.0"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Supported Operations
|
|
||||||
|
|
||||||
### Basic Arithmetic
|
|
||||||
- Addition: `(+ a b)` → `(decimal-add "a" "b")`
|
|
||||||
- Subtraction: `(- a b)` → `(decimal-sub "a" "b")`
|
|
||||||
- Multiplication: `(* a b)` → `(decimal-mul "a" "b")`
|
|
||||||
- Division: `(/ a b)` → `(decimal-div "a" "b")`
|
|
||||||
|
|
||||||
### Advanced Mathematics
|
|
||||||
- Power: `(^ base exp)` → `(decimal-pow "base" "exp")`
|
|
||||||
- Square root: `(sqrt x)` → `(decimal-sqrt "x")`
|
|
||||||
- Natural log: `(ln x)` → `(decimal-ln "x")`
|
|
||||||
- Base 10 log: `(log10 x)` → `(decimal-log10 "x")`
|
|
||||||
- Exponential: `(exp x)` → `(decimal-exp "x")`
|
|
||||||
|
|
||||||
### Trigonometric Functions
|
|
||||||
- `(sin x)`, `(cos x)`, `(tan x)`
|
|
||||||
|
|
||||||
### Comparison Operations
|
|
||||||
- `(> a b)`, `(< a b)`, `(= a b)`, `(>= a b)`, `(<= a b)`
|
|
||||||
|
|
||||||
### Financial Functions
|
|
||||||
- Percentage: `(decimal-percentage amount rate)`
|
|
||||||
- Compound interest: `(decimal-compound principal rate time)`
|
|
||||||
|
|
||||||
### Precision Control
|
|
||||||
- Set precision: `(set-precision 4)`
|
|
||||||
- Get current precision: `(get-precision)`
|
|
||||||
- Clear precision: `(clear-precision)`
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
The crate includes several examples:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Basic usage
|
|
||||||
cargo run --example basic_usage
|
|
||||||
|
|
||||||
# Selective function registration
|
|
||||||
cargo run --example selective_registration
|
|
||||||
|
|
||||||
# Working with variables
|
|
||||||
cargo run --example with_variables
|
|
||||||
```
|
|
||||||
|
|
||||||
## Scientific Notation Support
|
|
||||||
|
|
||||||
Steel Decimal supports scientific notation in decimal parsing:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
let script = "(+ 1.5e2 2.3e-1)"; // 150 + 0.23 = 150.23
|
|
||||||
let transformed = steel_decimal.transform(script);
|
|
||||||
```
|
|
||||||
|
|
||||||
## Thread Safety
|
|
||||||
|
|
||||||
Precision settings are thread-local, allowing different threads to use different precision contexts safely.
|
|
||||||
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
This project is licensed under either of
|
|
||||||
|
|
||||||
- Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0)
|
|
||||||
- MIT License ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)
|
|
||||||
|
|
||||||
at your option.
|
|
||||||
@@ -1,366 +0,0 @@
|
|||||||
# rust_decimal for Financial Applications: Complete Guide
|
|
||||||
|
|
||||||
rust_decimal provides a 128-bit fixed-precision decimal implementation designed specifically for financial calculations, eliminating floating-point rounding errors that plague traditional financial software. With a 96-bit mantissa and support for up to 28 decimal places, it offers the exact precision required for accounting and monetary calculations while maintaining performance suitable for high-throughput financial systems.
|
|
||||||
|
|
||||||
## Input handling best practices
|
|
||||||
|
|
||||||
### String parsing and validation patterns
|
|
||||||
|
|
||||||
rust_decimal provides multiple parsing methods optimized for different input scenarios. The **most robust approach for financial applications** uses `from_str_exact()` for strict validation combined with comprehensive error handling:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
use rust_decimal::{Decimal, Error};
|
|
||||||
|
|
||||||
fn parse_financial_amount(input: &str) -> Result<Decimal, AmountError> {
|
|
||||||
let trimmed = input.trim();
|
|
||||||
|
|
||||||
// Pre-validation checks
|
|
||||||
if trimmed.is_empty() {
|
|
||||||
return Err(AmountError::EmptyInput);
|
|
||||||
}
|
|
||||||
|
|
||||||
if trimmed.len() > 50 {
|
|
||||||
return Err(AmountError::InputTooLong);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use from_str_exact for strict parsing
|
|
||||||
Decimal::from_str_exact(trimmed)
|
|
||||||
.map_err(|e| match e {
|
|
||||||
Error::InvalidOperation => AmountError::InvalidFormat,
|
|
||||||
Error::Underflow => AmountError::Underflow,
|
|
||||||
Error::Overflow => AmountError::Overflow,
|
|
||||||
_ => AmountError::ParseError,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The library supports **multiple input formats automatically**: standard decimal notation (`"123.45"`), scientific notation via `from_scientific("2.512e1")`, and different radix bases through `from_str_radix()`. For **compile-time optimization**, use the `dec!()` macro which parses literals at compile time with zero runtime cost.
|
|
||||||
|
|
||||||
### Precision and currency considerations
|
|
||||||
|
|
||||||
Different financial contexts require specific precision strategies. **Standard recommendations** include 2-4 decimal places for retail/e-commerce, 4-6 for forex trading, 8-18 for cryptocurrency, and 4-6 for GAAP accounting compliance. The library's maximum scale of 28 decimal places accommodates even the most demanding financial calculations.
|
|
||||||
|
|
||||||
**Currency-specific validation patterns** should enforce appropriate ranges and scales:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
pub struct ValidatedAmount(Decimal);
|
|
||||||
|
|
||||||
impl ValidatedAmount {
|
|
||||||
pub fn new(value: Decimal) -> Result<Self, FinancialError> {
|
|
||||||
// Range validation
|
|
||||||
if value < Decimal::MIN || value > Decimal::MAX {
|
|
||||||
return Err(FinancialError::OutOfRange);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Precision validation for currency context
|
|
||||||
if value.scale() > 28 {
|
|
||||||
return Err(FinancialError::ScaleExceeded);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(ValidatedAmount(value))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Handling different input formats
|
|
||||||
|
|
||||||
For **production systems processing various input formats**, implement a unified parsing strategy that handles integers, decimals, and scientific notation:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// Automatic conversion from integers
|
|
||||||
let amount = Decimal::from(12345_i64); // 12345
|
|
||||||
|
|
||||||
// Float conversion with precision control
|
|
||||||
let price = Decimal::from_f64(123.45).unwrap();
|
|
||||||
|
|
||||||
// Scientific notation parsing
|
|
||||||
let large_amount = Decimal::from_scientific("1.23e6").unwrap(); // 1230000
|
|
||||||
|
|
||||||
// String parsing with validation
|
|
||||||
let user_input = "99.99";
|
|
||||||
let parsed = Decimal::from_str(user_input)?;
|
|
||||||
```
|
|
||||||
|
|
||||||
## Output formatting best practices
|
|
||||||
|
|
||||||
### Display and precision control
|
|
||||||
|
|
||||||
rust_decimal provides multiple formatting approaches optimized for different financial contexts. The **standard approach** uses the `Display` trait for human-readable output, while **precision-controlled formatting** uses `round_dp()` for specific decimal places:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
let amount = dec!(123.456789);
|
|
||||||
|
|
||||||
// Standard string representation
|
|
||||||
let display = amount.to_string(); // "123.456789"
|
|
||||||
|
|
||||||
// Precision-controlled output
|
|
||||||
let currency_format = amount.round_dp(2).to_string(); // "123.46"
|
|
||||||
|
|
||||||
// Scientific notation for large numbers
|
|
||||||
let scientific = format!("{:e}", amount); // "1.23456789e2"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Rounding strategies for accounting
|
|
||||||
|
|
||||||
The library implements **comprehensive rounding strategies** including banker's rounding (IEEE 754 compliant) which eliminates systematic bias in large datasets:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
use rust_decimal::RoundingStrategy;
|
|
||||||
|
|
||||||
let tax = dec!(3.4395);
|
|
||||||
|
|
||||||
// Banker's rounding (default) - preferred for financial compliance
|
|
||||||
let rounded = tax.round_dp(2); // Uses MidpointNearestEven
|
|
||||||
|
|
||||||
// Explicit rounding strategies
|
|
||||||
let away_from_zero = tax.round_dp_with_strategy(2, RoundingStrategy::MidpointAwayFromZero);
|
|
||||||
let truncated = tax.round_dp_with_strategy(2, RoundingStrategy::ToZero);
|
|
||||||
```
|
|
||||||
|
|
||||||
### Currency formatting and localization
|
|
||||||
|
|
||||||
For **multi-currency applications**, implement currency-aware formatting that maintains precision requirements:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct Money {
|
|
||||||
amount: Decimal,
|
|
||||||
currency: Currency,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Money {
|
|
||||||
pub fn format_for_display(&self, precision: u32) -> String {
|
|
||||||
match self.currency {
|
|
||||||
Currency::USD => format!("${}", self.amount.round_dp(precision)),
|
|
||||||
Currency::EUR => format!("€{}", self.amount.round_dp(precision)),
|
|
||||||
Currency::BTC => format!("₿{}", self.amount.round_dp(8)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Robust conversion patterns
|
|
||||||
|
|
||||||
### String-to-Decimal-to-String pipeline
|
|
||||||
|
|
||||||
The **most efficient conversion pipeline** for financial applications uses compile-time optimization where possible and validated parsing for runtime inputs:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// Compile-time optimization for known values
|
|
||||||
const COMMISSION_RATE: Decimal = dec!(0.0025);
|
|
||||||
const TAX_RATE: Decimal = dec!(0.15);
|
|
||||||
|
|
||||||
// Runtime parsing with validation
|
|
||||||
fn process_transaction(amount_str: &str) -> Result<String, ProcessingError> {
|
|
||||||
let amount = parse_financial_amount(amount_str)?;
|
|
||||||
let commission = amount * COMMISSION_RATE;
|
|
||||||
let total = amount + commission;
|
|
||||||
|
|
||||||
Ok(total.round_dp(2).to_string())
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Edge case handling
|
|
||||||
|
|
||||||
**Production-ready edge case handling** requires comprehensive validation and error recovery:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
pub trait SafeDecimalOps {
|
|
||||||
fn safe_add(&self, other: Self) -> Result<Self, FinancialError>;
|
|
||||||
fn safe_multiply(&self, other: Self) -> Result<Self, FinancialError>;
|
|
||||||
fn safe_divide(&self, other: Self) -> Result<Self, FinancialError>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SafeDecimalOps for Decimal {
|
|
||||||
fn safe_add(&self, other: Self) -> Result<Self, FinancialError> {
|
|
||||||
self.checked_add(other).ok_or(FinancialError::Overflow)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn safe_divide(&self, other: Self) -> Result<Self, FinancialError> {
|
|
||||||
if other.is_zero() {
|
|
||||||
return Err(FinancialError::DivisionByZero);
|
|
||||||
}
|
|
||||||
self.checked_div(other).ok_or(FinancialError::Overflow)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Performance considerations
|
|
||||||
|
|
||||||
For **high-frequency financial calculations**, rust_decimal offers significant advantages over floating-point arithmetic despite being 2-6x slower. **Key performance characteristics** include 10-20ns for addition/subtraction, 50-100ns for multiplication, and 100-200ns for division. The library uses **stack allocation** (16 bytes per Decimal) and provides **zero-cost abstractions** through compile-time macros.
|
|
||||||
|
|
||||||
**Memory optimization strategies** include using the `Copy` trait for efficient stack-based operations, implementing batch processing patterns, and pre-allocating constants:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// Efficient batch processing
|
|
||||||
fn calculate_portfolio_value(positions: &[Position]) -> Decimal {
|
|
||||||
positions.iter()
|
|
||||||
.map(|pos| pos.quantity * pos.average_price)
|
|
||||||
.sum() // Decimal implements Sum trait
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Financial-specific features
|
|
||||||
|
|
||||||
### Scale and precision handling
|
|
||||||
|
|
||||||
rust_decimal's **128-bit architecture** provides optimal balance between precision and performance for financial applications. The **96-bit mantissa** supports approximately 28-29 significant digits, while the **32-bit metadata** handles scale (0-28) and sign information.
|
|
||||||
|
|
||||||
**Database integration patterns** vary by storage backend:
|
|
||||||
- **PostgreSQL**: Use `NUMERIC(19,4)` for standard applications, `NUMERIC(28,8)` for high precision
|
|
||||||
- **MySQL**: Use `DECIMAL(13,4)` for general use, `DECIMAL(19,4)` for large amounts
|
|
||||||
- **GAAP compliance**: Often requires 4-6 decimal places with specific rounding rules
|
|
||||||
|
|
||||||
### Monetary arithmetic best practices
|
|
||||||
|
|
||||||
**Core principles for financial calculations** include always using rust_decimal instead of floating-point, maintaining consistent scale throughout calculations, using explicit rounding strategies, and implementing overflow protection:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// Safe financial calculation with tax
|
|
||||||
let subtotal = dec!(199.99);
|
|
||||||
let tax_rate = dec!(0.0875); // 8.75%
|
|
||||||
let tax = (subtotal * tax_rate).round_dp(2);
|
|
||||||
let total = subtotal.checked_add(tax).expect("Calculation overflow");
|
|
||||||
|
|
||||||
// Interest calculation with proper rounding
|
|
||||||
let principal = dec!(10000.00);
|
|
||||||
let rate = dec!(0.045); // 4.5% annual
|
|
||||||
let compound_interest = principal * (dec!(1) + rate / dec!(12)).powi(12);
|
|
||||||
let final_amount = compound_interest.round_dp(2);
|
|
||||||
```
|
|
||||||
|
|
||||||
### Integration with accounting systems
|
|
||||||
|
|
||||||
**Database integration** requires appropriate feature flags and schema design:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// PostgreSQL integration
|
|
||||||
[dependencies]
|
|
||||||
rust_decimal = { version = "1.37", features = ["db-postgres"] }
|
|
||||||
|
|
||||||
// Usage with tokio-postgres
|
|
||||||
let amount: Decimal = dec!(1234.56);
|
|
||||||
client.execute(
|
|
||||||
"INSERT INTO transactions (amount) VALUES ($1)",
|
|
||||||
&[&amount]
|
|
||||||
)?;
|
|
||||||
```
|
|
||||||
|
|
||||||
**JSON serialization** maintains precision through string-based representation:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
|
||||||
struct Invoice {
|
|
||||||
#[serde(with = "rust_decimal::serde::str")]
|
|
||||||
total: Decimal,
|
|
||||||
#[serde(with = "rust_decimal::serde::arbitrary_precision")]
|
|
||||||
tax: Decimal,
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Integration patterns
|
|
||||||
|
|
||||||
### Codebase integration strategies
|
|
||||||
|
|
||||||
**Domain-driven design patterns** provide robust abstractions for financial applications:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// Value object pattern for type safety
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
|
||||||
pub struct Balance(Decimal);
|
|
||||||
|
|
||||||
impl Balance {
|
|
||||||
pub fn new(amount: Decimal) -> Result<Self, Error> {
|
|
||||||
if amount < Decimal::ZERO {
|
|
||||||
return Err(Error::NegativeBalance);
|
|
||||||
}
|
|
||||||
Ok(Balance(amount))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add(&self, other: &Balance) -> Result<Balance, Error> {
|
|
||||||
let new_amount = self.0.checked_add(other.0)
|
|
||||||
.ok_or(Error::Overflow)?;
|
|
||||||
Ok(Balance(new_amount))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Aggregate root patterns** encapsulate business logic and maintain consistency:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
pub struct Account {
|
|
||||||
id: AccountId,
|
|
||||||
balance: Balance,
|
|
||||||
transactions: Vec<Transaction>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Account {
|
|
||||||
pub fn debit(&mut self, amount: Decimal) -> Result<(), DomainError> {
|
|
||||||
if self.balance.value() < amount {
|
|
||||||
return Err(DomainError::InsufficientFunds);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.balance = Balance::new(self.balance.value() - amount)?;
|
|
||||||
self.transactions.push(Transaction::debit(amount));
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Testing approaches
|
|
||||||
|
|
||||||
**Property-based testing** with proptest ensures mathematical correctness:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
use proptest::prelude::*;
|
|
||||||
|
|
||||||
proptest! {
|
|
||||||
#[test]
|
|
||||||
fn test_addition_commutative(a in any::<Decimal>(), b in any::<Decimal>()) {
|
|
||||||
prop_assert_eq!(a + b, b + a);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_compound_interest_monotonic(
|
|
||||||
principal in 0.01f64..1000000.0,
|
|
||||||
rate in 0.001f64..0.5,
|
|
||||||
periods in 1u32..100
|
|
||||||
) {
|
|
||||||
let p = Decimal::from_f64(principal).unwrap();
|
|
||||||
let r = Decimal::from_f64(rate).unwrap();
|
|
||||||
let result = p * (Decimal::ONE + r).powi(periods as i64);
|
|
||||||
|
|
||||||
// Property: compound interest should always be >= principal
|
|
||||||
prop_assert!(result >= p);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Error handling strategies
|
|
||||||
|
|
||||||
**Comprehensive error handling** uses the `thiserror` crate for production systems:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
|
||||||
pub enum FinancialError {
|
|
||||||
#[error("Insufficient funds: available {available}, required {required}")]
|
|
||||||
InsufficientFunds { available: Decimal, required: Decimal },
|
|
||||||
|
|
||||||
#[error("Currency mismatch: expected {expected}, got {actual}")]
|
|
||||||
CurrencyMismatch { expected: String, actual: String },
|
|
||||||
|
|
||||||
#[error("Precision overflow in calculation")]
|
|
||||||
PrecisionOverflow,
|
|
||||||
|
|
||||||
#[error("Division by zero")]
|
|
||||||
DivisionByZero,
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Conclusion
|
|
||||||
|
|
||||||
rust_decimal provides a mature, production-ready foundation for financial applications requiring exact precision. Its 128-bit fixed-precision architecture, comprehensive rounding strategies, and extensive ecosystem integration make it ideal for everything from simple e-commerce transactions to complex multi-currency trading platforms. The library's emphasis on correctness over raw performance, combined with Rust's memory safety guarantees, creates a robust platform for mission-critical financial systems where precision errors can have significant monetary consequences.
|
|
||||||
|
|
||||||
The key to successful implementation lies in proper domain modeling, comprehensive error handling, appropriate precision management, and thorough testing including property-based testing for financial invariants. With these practices, rust_decimal serves as a reliable foundation for financial software systems handling high-throughput transaction processing while maintaining the exact precision required for regulatory compliance and accounting accuracy.
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
// examples/basic_usage.rs
|
|
||||||
use steel_decimal::SteelDecimal;
|
|
||||||
use steel::steel_vm::engine::Engine;
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
// Create a new Steel Decimal engine
|
|
||||||
let steel_decimal = SteelDecimal::new();
|
|
||||||
|
|
||||||
// Transform a simple math expression
|
|
||||||
let script = "(+ 1.5 2.3)";
|
|
||||||
let transformed = steel_decimal.transform(script);
|
|
||||||
println!("Original: {}", script);
|
|
||||||
println!("Transformed: {}", transformed);
|
|
||||||
|
|
||||||
// Create Steel VM and register functions
|
|
||||||
let mut vm = Engine::new();
|
|
||||||
steel_decimal.register_functions(&mut vm);
|
|
||||||
|
|
||||||
// Execute the transformed script
|
|
||||||
match vm.compile_and_run_raw_program(transformed) {
|
|
||||||
Ok(results) => {
|
|
||||||
println!("Results: {:?}", results);
|
|
||||||
if let Some(last_result) = results.last() {
|
|
||||||
println!("Final result: {:?}", last_result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
println!("Error: {}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try a more complex expression
|
|
||||||
let complex_script = "(+ (* 2.5 3.0) (/ 15.0 3.0))";
|
|
||||||
let complex_transformed = steel_decimal.transform(complex_script);
|
|
||||||
println!("\nComplex original: {}", complex_script);
|
|
||||||
println!("Complex transformed: {}", complex_transformed);
|
|
||||||
|
|
||||||
match vm.compile_and_run_raw_program(complex_transformed) {
|
|
||||||
Ok(results) => {
|
|
||||||
if let Some(last_result) = results.last() {
|
|
||||||
println!("Complex result: {:?}", last_result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
println!("Error: {}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Using the convenience method
|
|
||||||
println!("\nUsing convenience method:");
|
|
||||||
match steel_decimal.parse_and_execute("(+ 10.5 20.3)") {
|
|
||||||
Ok(results) => {
|
|
||||||
if let Some(last_result) = results.last() {
|
|
||||||
println!("Convenience result: {:?}", last_result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
println!("Error: {}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
// examples/selective_registration.rs
|
|
||||||
use steel_decimal::{SteelDecimal, FunctionRegistryBuilder};
|
|
||||||
use steel::steel_vm::engine::Engine;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
let steel_decimal = SteelDecimal::new();
|
|
||||||
|
|
||||||
println!("=== Basic Arithmetic Only ===");
|
|
||||||
let mut vm1 = Engine::new();
|
|
||||||
FunctionRegistryBuilder::new()
|
|
||||||
.basic_arithmetic(true)
|
|
||||||
.advanced_math(false)
|
|
||||||
.trigonometric(false)
|
|
||||||
.comparison(false)
|
|
||||||
.utility(false)
|
|
||||||
.constants(false)
|
|
||||||
.financial(false)
|
|
||||||
.register(&mut vm1);
|
|
||||||
|
|
||||||
// Test basic arithmetic
|
|
||||||
let script = "(+ 10.5 20.3)";
|
|
||||||
let transformed = steel_decimal.transform(script);
|
|
||||||
match vm1.compile_and_run_raw_program(transformed) {
|
|
||||||
Ok(results) => {
|
|
||||||
if let Some(result) = results.last() {
|
|
||||||
println!("Basic arithmetic result: {:?}", result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => println!("Error: {}", e),
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("\n=== With Advanced Math ===");
|
|
||||||
let mut vm2 = Engine::new();
|
|
||||||
FunctionRegistryBuilder::new()
|
|
||||||
.basic_arithmetic(true)
|
|
||||||
.advanced_math(true)
|
|
||||||
.trigonometric(false)
|
|
||||||
.register(&mut vm2);
|
|
||||||
|
|
||||||
// Test power function
|
|
||||||
let power_script = "(^ 2.0 3.0)";
|
|
||||||
let power_transformed = steel_decimal.transform(power_script);
|
|
||||||
match vm2.compile_and_run_raw_program(power_transformed) {
|
|
||||||
Ok(results) => {
|
|
||||||
if let Some(result) = results.last() {
|
|
||||||
println!("Power result: {:?}", result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => println!("Error: {}", e),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test square root
|
|
||||||
let sqrt_script = "(sqrt 16.0)";
|
|
||||||
let sqrt_transformed = steel_decimal.transform(sqrt_script);
|
|
||||||
match vm2.compile_and_run_raw_program(sqrt_transformed) {
|
|
||||||
Ok(results) => {
|
|
||||||
if let Some(result) = results.last() {
|
|
||||||
println!("Square root result: {:?}", result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => println!("Error: {}", e),
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("\n=== With Variables ===");
|
|
||||||
let mut variables = HashMap::new();
|
|
||||||
variables.insert("radius".to_string(), "5.0".to_string());
|
|
||||||
variables.insert("pi".to_string(), "3.14159".to_string());
|
|
||||||
|
|
||||||
let mut vm3 = Engine::new();
|
|
||||||
FunctionRegistryBuilder::new()
|
|
||||||
.basic_arithmetic(true)
|
|
||||||
.advanced_math(true)
|
|
||||||
.constants(true)
|
|
||||||
.with_variables(variables)
|
|
||||||
.register(&mut vm3);
|
|
||||||
|
|
||||||
// Calculate area of circle using variables
|
|
||||||
let area_script = "(* $pi (* $radius $radius))";
|
|
||||||
let area_transformed = steel_decimal.transform(area_script);
|
|
||||||
println!("Area script: {}", area_script);
|
|
||||||
println!("Transformed: {}", area_transformed);
|
|
||||||
|
|
||||||
match vm3.compile_and_run_raw_program(area_transformed) {
|
|
||||||
Ok(results) => {
|
|
||||||
if let Some(result) = results.last() {
|
|
||||||
println!("Circle area result: {:?}", result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => println!("Error: {}", e),
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("\n=== Financial Functions ===");
|
|
||||||
let mut vm4 = Engine::new();
|
|
||||||
FunctionRegistryBuilder::new()
|
|
||||||
.basic_arithmetic(true)
|
|
||||||
.financial(true)
|
|
||||||
.register(&mut vm4);
|
|
||||||
|
|
||||||
// Test percentage calculation
|
|
||||||
let percent_script = r#"(decimal-percentage "1000.00" "15.0")"#;
|
|
||||||
match vm4.compile_and_run_raw_program(percent_script.to_string()) {
|
|
||||||
Ok(results) => {
|
|
||||||
if let Some(result) = results.last() {
|
|
||||||
println!("15% of 1000: {:?}", result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => println!("Error: {}", e),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test compound interest
|
|
||||||
let compound_script = r#"(decimal-compound "1000.00" "0.05" "10.0")"#;
|
|
||||||
match vm4.compile_and_run_raw_program(compound_script.to_string()) {
|
|
||||||
Ok(results) => {
|
|
||||||
if let Some(result) = results.last() {
|
|
||||||
println!("Compound interest (1000 @ 5% for 10 years): {:?}", result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => println!("Error: {}", e),
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("\n=== Available Functions ===");
|
|
||||||
let function_names = steel_decimal::FunctionRegistry::get_function_names();
|
|
||||||
for (i, name) in function_names.iter().enumerate() {
|
|
||||||
if i % 5 == 0 {
|
|
||||||
println!();
|
|
||||||
}
|
|
||||||
print!("{:<18}", name);
|
|
||||||
}
|
|
||||||
println!();
|
|
||||||
}
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
// examples/with_variables.rs
|
|
||||||
use steel_decimal::SteelDecimal;
|
|
||||||
use steel::steel_vm::engine::Engine;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
// Create variables
|
|
||||||
let mut variables = HashMap::new();
|
|
||||||
variables.insert("price".to_string(), "29.99".to_string());
|
|
||||||
variables.insert("quantity".to_string(), "5".to_string());
|
|
||||||
variables.insert("tax_rate".to_string(), "0.08".to_string());
|
|
||||||
|
|
||||||
// Create Steel Decimal with variables
|
|
||||||
let steel_decimal = SteelDecimal::with_variables(variables);
|
|
||||||
|
|
||||||
// Script using variables
|
|
||||||
let script = "(+ (* $price $quantity) (* (* $price $quantity) $tax_rate))";
|
|
||||||
let transformed = steel_decimal.transform(script);
|
|
||||||
|
|
||||||
println!("Original script: {}", script);
|
|
||||||
println!("Transformed script: {}", transformed);
|
|
||||||
|
|
||||||
// Create VM and register functions
|
|
||||||
let mut vm = Engine::new();
|
|
||||||
steel_decimal.register_functions(&mut vm);
|
|
||||||
|
|
||||||
// Execute the script
|
|
||||||
match vm.compile_and_run_raw_program(transformed) {
|
|
||||||
Ok(results) => {
|
|
||||||
if let Some(last_result) = results.last() {
|
|
||||||
println!("Total with tax: {:?}", last_result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
println!("Error: {}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Demonstrate adding variables dynamically
|
|
||||||
let mut steel_decimal = SteelDecimal::new();
|
|
||||||
steel_decimal.add_variable("x".to_string(), "10.5".to_string());
|
|
||||||
steel_decimal.add_variable("y".to_string(), "20.3".to_string());
|
|
||||||
|
|
||||||
let simple_script = "(+ $x $y)";
|
|
||||||
println!("\nSimple script: {}", simple_script);
|
|
||||||
|
|
||||||
match steel_decimal.parse_and_execute(simple_script) {
|
|
||||||
Ok(results) => {
|
|
||||||
if let Some(last_result) = results.last() {
|
|
||||||
println!("Simple result: {:?}", last_result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
println!("Error: {}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show variable validation
|
|
||||||
println!("\nVariable validation:");
|
|
||||||
match steel_decimal.validate_script("(+ $x $y)") {
|
|
||||||
Ok(()) => println!("Script is valid"),
|
|
||||||
Err(e) => println!("Script error: {}", e),
|
|
||||||
}
|
|
||||||
|
|
||||||
match steel_decimal.validate_script("(+ $x $undefined_var)") {
|
|
||||||
Ok(()) => println!("Script is valid"),
|
|
||||||
Err(e) => println!("Script error: {}", e),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract dependencies
|
|
||||||
let dependencies = steel_decimal.extract_dependencies("(+ $x $y $z)");
|
|
||||||
println!("Dependencies: {:?}", dependencies);
|
|
||||||
}
|
|
||||||
@@ -1,320 +0,0 @@
|
|||||||
// src/functions.rs
|
|
||||||
use rust_decimal::prelude::*;
|
|
||||||
use rust_decimal::MathematicalOps;
|
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
// Global precision setting for the current Steel execution context
|
|
||||||
thread_local! {
|
|
||||||
static PRECISION_CONTEXT: std::cell::RefCell<Option<u32>> = std::cell::RefCell::new(None);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set execution precision for all decimal operations in current thread
|
|
||||||
pub fn set_execution_precision(precision: Option<u32>) {
|
|
||||||
PRECISION_CONTEXT.with(|p| *p.borrow_mut() = precision);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get current execution precision
|
|
||||||
pub fn get_execution_precision() -> Option<u32> {
|
|
||||||
PRECISION_CONTEXT.with(|p| *p.borrow())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Format decimal according to current execution context
|
|
||||||
fn format_result(decimal: Decimal) -> String {
|
|
||||||
match get_execution_precision() {
|
|
||||||
Some(precision) => decimal.round_dp(precision).to_string(),
|
|
||||||
None => decimal.to_string(), // Full precision (default behavior)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Helper function to parse decimals with strict accounting precision
|
|
||||||
/// Supports both standard decimal notation AND scientific notation
|
|
||||||
fn parse_decimal(s: &str) -> Result<Decimal, String> {
|
|
||||||
// First try direct parsing for regular decimals
|
|
||||||
if let Ok(decimal) = Decimal::from_str(s) {
|
|
||||||
return Ok(decimal);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for scientific notation
|
|
||||||
if s.contains('e') || s.contains('E') {
|
|
||||||
return parse_scientific_notation(s);
|
|
||||||
}
|
|
||||||
|
|
||||||
Err(format!("Invalid decimal '{}': unknown format", s))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parse scientific notation (e.g., "1e2", "1.5e-3") using decimal arithmetic
|
|
||||||
fn parse_scientific_notation(s: &str) -> Result<Decimal, String> {
|
|
||||||
let lower_s = s.to_lowercase();
|
|
||||||
let parts: Vec<&str> = lower_s.split('e').collect();
|
|
||||||
if parts.len() != 2 {
|
|
||||||
return Err(format!("Invalid scientific notation '{}': malformed", s));
|
|
||||||
}
|
|
||||||
|
|
||||||
let mantissa = Decimal::from_str(parts[0])
|
|
||||||
.map_err(|_| format!("Invalid mantissa in '{}': {}", s, parts[0]))?;
|
|
||||||
let exponent: i32 = parts[1].parse()
|
|
||||||
.map_err(|_| format!("Invalid exponent in '{}': {}", s, parts[1]))?;
|
|
||||||
|
|
||||||
let result = if exponent == 0 {
|
|
||||||
mantissa
|
|
||||||
} else if exponent > 0 {
|
|
||||||
let ten = Decimal::from(10);
|
|
||||||
let power_of_ten = ten.checked_powi(exponent as i64)
|
|
||||||
.ok_or_else(|| format!("Exponent too large in '{}': {}", s, exponent))?;
|
|
||||||
mantissa.checked_mul(power_of_ten)
|
|
||||||
.ok_or_else(|| format!("Scientific notation result overflow in '{}'", s))?
|
|
||||||
} else {
|
|
||||||
let ten = Decimal::from(10);
|
|
||||||
let positive_exp = (-exponent) as i64;
|
|
||||||
let divisor = ten.checked_powi(positive_exp)
|
|
||||||
.ok_or_else(|| format!("Exponent too large in '{}': {}", s, exponent))?;
|
|
||||||
mantissa.checked_div(divisor)
|
|
||||||
.ok_or_else(|| format!("Scientific notation result underflow in '{}'", s))?
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Basic arithmetic operations
|
|
||||||
pub fn decimal_add(a: String, b: String) -> Result<String, String> {
|
|
||||||
let a_dec = parse_decimal(&a)?;
|
|
||||||
let b_dec = parse_decimal(&b)?;
|
|
||||||
|
|
||||||
a_dec.checked_add(b_dec)
|
|
||||||
.map(|result| format_result(result))
|
|
||||||
.ok_or_else(|| "Addition overflowed".to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn decimal_sub(a: String, b: String) -> Result<String, String> {
|
|
||||||
let a_dec = parse_decimal(&a)?;
|
|
||||||
let b_dec = parse_decimal(&b)?;
|
|
||||||
|
|
||||||
a_dec.checked_sub(b_dec)
|
|
||||||
.map(|result| format_result(result))
|
|
||||||
.ok_or_else(|| "Subtraction overflowed".to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn decimal_mul(a: String, b: String) -> Result<String, String> {
|
|
||||||
let a_dec = parse_decimal(&a)?;
|
|
||||||
let b_dec = parse_decimal(&b)?;
|
|
||||||
|
|
||||||
a_dec.checked_mul(b_dec)
|
|
||||||
.map(|result| format_result(result))
|
|
||||||
.ok_or_else(|| "Multiplication overflowed".to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn decimal_div(a: String, b: String) -> Result<String, String> {
|
|
||||||
let a_dec = parse_decimal(&a)?;
|
|
||||||
let b_dec = parse_decimal(&b)?;
|
|
||||||
|
|
||||||
if b_dec.is_zero() {
|
|
||||||
return Err("Division by zero".to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
a_dec.checked_div(b_dec)
|
|
||||||
.map(|result| format_result(result))
|
|
||||||
.ok_or_else(|| "Division overflowed".to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Precision control functions
|
|
||||||
pub fn set_precision(precision: u32) -> String {
|
|
||||||
if precision > 28 {
|
|
||||||
"Error: Maximum precision is 28 decimal places".to_string()
|
|
||||||
} else {
|
|
||||||
set_execution_precision(Some(precision as u32));
|
|
||||||
format!("Precision set to {} decimal places", precision)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_precision() -> String {
|
|
||||||
match get_execution_precision() {
|
|
||||||
Some(p) => p.to_string(),
|
|
||||||
None => "full".to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn clear_precision() -> String {
|
|
||||||
set_execution_precision(None);
|
|
||||||
"Precision cleared - using full precision".to_string()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Format functions with explicit precision
|
|
||||||
pub fn decimal_format(value: String, precision: u32) -> Result<String, String> {
|
|
||||||
let decimal = parse_decimal(&value)?;
|
|
||||||
|
|
||||||
if precision > 28 {
|
|
||||||
Err("Maximum precision is 28 decimal places".to_string())
|
|
||||||
} else {
|
|
||||||
Ok(decimal.round_dp(precision as u32).to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Advanced mathematical functions (updated to use format_result)
|
|
||||||
pub fn decimal_pow(base: String, exp: String) -> Result<String, String> {
|
|
||||||
let base_dec = parse_decimal(&base)?;
|
|
||||||
let exp_dec = parse_decimal(&exp)?;
|
|
||||||
|
|
||||||
base_dec.checked_powd(exp_dec)
|
|
||||||
.map(|result| format_result(result))
|
|
||||||
.ok_or_else(|| "Power operation failed or overflowed".to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn decimal_sqrt(a: String) -> Result<String, String> {
|
|
||||||
let a_dec = parse_decimal(&a)?;
|
|
||||||
|
|
||||||
a_dec.sqrt()
|
|
||||||
.map(|result| format_result(result))
|
|
||||||
.ok_or_else(|| "Square root failed (negative number?)".to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn decimal_ln(a: String) -> Result<String, String> {
|
|
||||||
let a_dec = parse_decimal(&a)?;
|
|
||||||
|
|
||||||
a_dec.checked_ln()
|
|
||||||
.map(|result| format_result(result))
|
|
||||||
.ok_or_else(|| "Natural log failed (non-positive number?)".to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn decimal_log10(a: String) -> Result<String, String> {
|
|
||||||
let a_dec = parse_decimal(&a)?;
|
|
||||||
|
|
||||||
a_dec.checked_log10()
|
|
||||||
.map(|result| format_result(result))
|
|
||||||
.ok_or_else(|| "Log10 failed (non-positive number?)".to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn decimal_exp(a: String) -> Result<String, String> {
|
|
||||||
let a_dec = parse_decimal(&a)?;
|
|
||||||
|
|
||||||
a_dec.checked_exp()
|
|
||||||
.map(|result| format_result(result))
|
|
||||||
.ok_or_else(|| "Exponential failed or overflowed".to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Trigonometric functions (updated to use format_result)
|
|
||||||
pub fn decimal_sin(a: String) -> Result<String, String> {
|
|
||||||
let a_dec = parse_decimal(&a)?;
|
|
||||||
|
|
||||||
a_dec.checked_sin()
|
|
||||||
.map(|result| format_result(result))
|
|
||||||
.ok_or_else(|| "Sine calculation failed or overflowed".to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn decimal_cos(a: String) -> Result<String, String> {
|
|
||||||
let a_dec = parse_decimal(&a)?;
|
|
||||||
|
|
||||||
a_dec.checked_cos()
|
|
||||||
.map(|result| format_result(result))
|
|
||||||
.ok_or_else(|| "Cosine calculation failed or overflowed".to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn decimal_tan(a: String) -> Result<String, String> {
|
|
||||||
let a_dec = parse_decimal(&a)?;
|
|
||||||
|
|
||||||
a_dec.checked_tan()
|
|
||||||
.map(|result| format_result(result))
|
|
||||||
.ok_or_else(|| "Tangent calculation failed or overflowed".to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Comparison functions (unchanged)
|
|
||||||
pub fn decimal_gt(a: String, b: String) -> Result<bool, String> {
|
|
||||||
let a_dec = parse_decimal(&a)?;
|
|
||||||
let b_dec = parse_decimal(&b)?;
|
|
||||||
Ok(a_dec > b_dec)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn decimal_gte(a: String, b: String) -> Result<bool, String> {
|
|
||||||
let a_dec = parse_decimal(&a)?;
|
|
||||||
let b_dec = parse_decimal(&b)?;
|
|
||||||
Ok(a_dec >= b_dec)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn decimal_lt(a: String, b: String) -> Result<bool, String> {
|
|
||||||
let a_dec = parse_decimal(&a)?;
|
|
||||||
let b_dec = parse_decimal(&b)?;
|
|
||||||
Ok(a_dec < b_dec)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn decimal_lte(a: String, b: String) -> Result<bool, String> {
|
|
||||||
let a_dec = parse_decimal(&a)?;
|
|
||||||
let b_dec = parse_decimal(&b)?;
|
|
||||||
Ok(a_dec <= b_dec)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn decimal_eq(a: String, b: String) -> Result<bool, String> {
|
|
||||||
let a_dec = parse_decimal(&a)?;
|
|
||||||
let b_dec = parse_decimal(&b)?;
|
|
||||||
Ok(a_dec == b_dec)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Utility functions (updated to use format_result)
|
|
||||||
pub fn decimal_abs(a: String) -> Result<String, String> {
|
|
||||||
let a_dec = parse_decimal(&a)?;
|
|
||||||
Ok(format_result(a_dec.abs()))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn decimal_round(a: String, places: i32) -> Result<String, String> {
|
|
||||||
let a_dec = parse_decimal(&a)?;
|
|
||||||
if places < 0 {
|
|
||||||
Ok(a_dec.to_string())
|
|
||||||
} else {
|
|
||||||
Ok(a_dec.round_dp(places as u32).to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn decimal_min(a: String, b: String) -> Result<String, String> {
|
|
||||||
let a_dec = parse_decimal(&a)?;
|
|
||||||
let b_dec = parse_decimal(&b)?;
|
|
||||||
Ok(format_result(a_dec.min(b_dec)))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn decimal_max(a: String, b: String) -> Result<String, String> {
|
|
||||||
let a_dec = parse_decimal(&a)?;
|
|
||||||
let b_dec = parse_decimal(&b)?;
|
|
||||||
Ok(format_result(a_dec.max(b_dec)))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Constants (unchanged)
|
|
||||||
pub fn decimal_zero() -> String {
|
|
||||||
"0".to_string()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn decimal_one() -> String {
|
|
||||||
"1".to_string()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn decimal_pi() -> String {
|
|
||||||
"3.1415926535897932384626433833".to_string()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn decimal_e() -> String {
|
|
||||||
"2.7182818284590452353602874714".to_string()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Financial functions (updated to use format_result)
|
|
||||||
pub fn decimal_percentage(amount: String, percentage: String) -> Result<String, String> {
|
|
||||||
let amount_dec = parse_decimal(&amount)?;
|
|
||||||
let percentage_dec = parse_decimal(&percentage)?;
|
|
||||||
let hundred = Decimal::from(100);
|
|
||||||
|
|
||||||
Ok(format_result(amount_dec * percentage_dec / hundred))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn decimal_compound(principal: String, rate: String, time: String) -> Result<String, String> {
|
|
||||||
let principal_dec = parse_decimal(&principal)?;
|
|
||||||
let rate_dec = parse_decimal(&rate)?;
|
|
||||||
let time_dec = parse_decimal(&time)?;
|
|
||||||
|
|
||||||
let one = Decimal::ONE;
|
|
||||||
let compound_factor = (one + rate_dec).checked_powd(time_dec)
|
|
||||||
.ok_or("Compound calculation overflow")?;
|
|
||||||
|
|
||||||
Ok(format_result(principal_dec * compound_factor))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Type conversion helper (updated to use format_result)
|
|
||||||
pub fn to_decimal(s: String) -> Result<String, String> {
|
|
||||||
parse_decimal(&s)
|
|
||||||
.map(|d| format_result(d))
|
|
||||||
.map_err(|e| format!("Invalid decimal: {}", e))
|
|
||||||
}
|
|
||||||
@@ -1,186 +0,0 @@
|
|||||||
// src/lib.rs
|
|
||||||
//! # Steel Decimal
|
|
||||||
//!
|
|
||||||
//! A Rust library that provides decimal arithmetic support for the Steel programming language.
|
|
||||||
//! This crate transforms Steel scripts to use high-precision decimal operations and provides
|
|
||||||
//! function registration utilities for Steel VMs.
|
|
||||||
//!
|
|
||||||
//! ## Quick Start
|
|
||||||
//!
|
|
||||||
//! ### Basic Usage
|
|
||||||
//! ```rust
|
|
||||||
//! use steel_decimal::SteelDecimal;
|
|
||||||
//! use steel::steel_vm::engine::Engine;
|
|
||||||
//!
|
|
||||||
//! // Create a new Steel Decimal engine
|
|
||||||
//! let steel_decimal = SteelDecimal::new();
|
|
||||||
//!
|
|
||||||
//! // Transform a script
|
|
||||||
//! let script = "(+ 1.5 2.3)";
|
|
||||||
//! let transformed = steel_decimal.transform(script);
|
|
||||||
//! // Result: "(decimal-add \"1.5\" \"2.3\")"
|
|
||||||
//!
|
|
||||||
//! // Register functions with Steel VM
|
|
||||||
//! let mut vm = Engine::new();
|
|
||||||
//! steel_decimal.register_functions(&mut vm);
|
|
||||||
//!
|
|
||||||
//! // Now you can execute the transformed script
|
|
||||||
//! let result = vm.compile_and_run_raw_program(transformed);
|
|
||||||
//! ```
|
|
||||||
//!
|
|
||||||
//! ### With Variables
|
|
||||||
//! ```rust
|
|
||||||
//! use steel_decimal::SteelDecimal;
|
|
||||||
//! use steel::steel_vm::engine::Engine;
|
|
||||||
//! use std::collections::HashMap;
|
|
||||||
//!
|
|
||||||
//! let mut variables = HashMap::new();
|
|
||||||
//! variables.insert("x".to_string(), "10.5".to_string());
|
|
||||||
//! variables.insert("y".to_string(), "20.3".to_string());
|
|
||||||
//!
|
|
||||||
//! let steel_decimal = SteelDecimal::with_variables(variables);
|
|
||||||
//!
|
|
||||||
//! let script = "(+ $x $y)";
|
|
||||||
//! let transformed = steel_decimal.transform(script);
|
|
||||||
//!
|
|
||||||
//! let mut vm = Engine::new();
|
|
||||||
//! steel_decimal.register_functions(&mut vm);
|
|
||||||
//! ```
|
|
||||||
//!
|
|
||||||
//! ### Selective Function Registration
|
|
||||||
//! ```rust
|
|
||||||
//! use steel_decimal::FunctionRegistryBuilder;
|
|
||||||
//! use steel::steel_vm::engine::Engine;
|
|
||||||
//!
|
|
||||||
//! let mut vm = Engine::new();
|
|
||||||
//! FunctionRegistryBuilder::new()
|
|
||||||
//! .basic_arithmetic(true)
|
|
||||||
//! .advanced_math(false)
|
|
||||||
//! .trigonometric(false)
|
|
||||||
//! .register(&mut vm);
|
|
||||||
//! ```
|
|
||||||
|
|
||||||
pub mod functions;
|
|
||||||
pub mod parser;
|
|
||||||
pub mod registry;
|
|
||||||
pub mod utils;
|
|
||||||
|
|
||||||
pub use functions::*;
|
|
||||||
pub use parser::ScriptParser;
|
|
||||||
pub use registry::{FunctionRegistry, FunctionRegistryBuilder};
|
|
||||||
pub use utils::{TypeConverter, ScriptAnalyzer, DecimalPrecision, ConversionError};
|
|
||||||
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use steel::steel_vm::engine::Engine;
|
|
||||||
|
|
||||||
/// Main entry point for the Steel Decimal library
|
|
||||||
pub struct SteelDecimal {
|
|
||||||
parser: ScriptParser,
|
|
||||||
variables: HashMap<String, String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SteelDecimal {
|
|
||||||
/// Create a new SteelDecimal instance
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
parser: ScriptParser::new(),
|
|
||||||
variables: HashMap::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a new SteelDecimal instance with variables
|
|
||||||
pub fn with_variables(variables: HashMap<String, String>) -> Self {
|
|
||||||
Self {
|
|
||||||
parser: ScriptParser::new(),
|
|
||||||
variables,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Transform a script by converting math operations to decimal functions
|
|
||||||
pub fn transform(&self, script: &str) -> String {
|
|
||||||
self.parser.transform(script)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Register all decimal functions with a Steel VM
|
|
||||||
pub fn register_functions(&self, vm: &mut Engine) {
|
|
||||||
FunctionRegistry::register_all(vm);
|
|
||||||
|
|
||||||
if !self.variables.is_empty() {
|
|
||||||
FunctionRegistry::register_variables(vm, self.variables.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Register functions selectively using a builder
|
|
||||||
pub fn register_functions_with_builder(&self, vm: &mut Engine, builder: FunctionRegistryBuilder) {
|
|
||||||
let builder = if !self.variables.is_empty() {
|
|
||||||
builder.with_variables(self.variables.clone())
|
|
||||||
} else {
|
|
||||||
builder
|
|
||||||
};
|
|
||||||
|
|
||||||
builder.register(vm);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add a variable to the context
|
|
||||||
pub fn add_variable(&mut self, name: String, value: String) {
|
|
||||||
self.variables.insert(name, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get all variables
|
|
||||||
pub fn get_variables(&self) -> &HashMap<String, String> {
|
|
||||||
&self.variables
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Extract dependencies from a script
|
|
||||||
pub fn extract_dependencies(&self, script: &str) -> std::collections::HashSet<String> {
|
|
||||||
self.parser.extract_dependencies(script)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parse and execute a script in one step (convenience method)
|
|
||||||
pub fn parse_and_execute(&self, script: &str) -> Result<Vec<steel::rvals::SteelVal>, String> {
|
|
||||||
let mut vm = Engine::new();
|
|
||||||
self.register_functions(&mut vm);
|
|
||||||
|
|
||||||
let transformed = self.transform(script);
|
|
||||||
|
|
||||||
vm.compile_and_run_raw_program(transformed)
|
|
||||||
.map_err(|e| e.to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Validate that a script can be transformed without errors
|
|
||||||
pub fn validate_script(&self, script: &str) -> Result<(), String> {
|
|
||||||
// Basic validation - check if transformation succeeds
|
|
||||||
let transformed = self.transform(script);
|
|
||||||
|
|
||||||
// Check if the script contains balanced parentheses
|
|
||||||
let open_count = transformed.chars().filter(|c| *c == '(').count();
|
|
||||||
let close_count = transformed.chars().filter(|c| *c == ')').count();
|
|
||||||
|
|
||||||
if open_count != close_count {
|
|
||||||
return Err("Unbalanced parentheses in script".to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if all variables are defined
|
|
||||||
let dependencies = self.extract_dependencies(script);
|
|
||||||
for dep in dependencies {
|
|
||||||
if !self.variables.contains_key(&dep) {
|
|
||||||
return Err(format!("Undefined variable: {}", dep));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for SteelDecimal {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Convenience functions for quick operations
|
|
||||||
pub mod prelude {
|
|
||||||
pub use crate::{SteelDecimal, FunctionRegistry, FunctionRegistryBuilder};
|
|
||||||
pub use crate::functions::*;
|
|
||||||
pub use crate::utils::{TypeConverter, ScriptAnalyzer, DecimalPrecision};
|
|
||||||
}
|
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
// src/parser.rs
|
|
||||||
use regex::Regex;
|
|
||||||
use std::collections::HashSet;
|
|
||||||
|
|
||||||
pub struct ScriptParser {
|
|
||||||
math_operators: Vec<(Regex, &'static str)>,
|
|
||||||
number_re: Regex,
|
|
||||||
variable_re: Regex,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ScriptParser {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
let math_operators = vec![
|
|
||||||
// Basic arithmetic
|
|
||||||
(Regex::new(r"\(\s*\+\s+").unwrap(), "(decimal-add "),
|
|
||||||
(Regex::new(r"\(\s*-\s+").unwrap(), "(decimal-sub "),
|
|
||||||
(Regex::new(r"\(\s*\*\s+").unwrap(), "(decimal-mul "),
|
|
||||||
(Regex::new(r"\(\s*/\s+").unwrap(), "(decimal-div "),
|
|
||||||
|
|
||||||
// Power and advanced operations
|
|
||||||
(Regex::new(r"\(\s*\^\s+").unwrap(), "(decimal-pow "),
|
|
||||||
(Regex::new(r"\(\s*\*\*\s+").unwrap(), "(decimal-pow "),
|
|
||||||
(Regex::new(r"\(\s*pow\s+").unwrap(), "(decimal-pow "),
|
|
||||||
(Regex::new(r"\(\s*sqrt\s+").unwrap(), "(decimal-sqrt "),
|
|
||||||
|
|
||||||
// Logarithmic functions
|
|
||||||
(Regex::new(r"\(\s*ln\s+").unwrap(), "(decimal-ln "),
|
|
||||||
(Regex::new(r"\(\s*log\s+").unwrap(), "(decimal-ln "),
|
|
||||||
(Regex::new(r"\(\s*log10\s+").unwrap(), "(decimal-log10 "),
|
|
||||||
(Regex::new(r"\(\s*exp\s+").unwrap(), "(decimal-exp "),
|
|
||||||
|
|
||||||
// Trigonometric functions
|
|
||||||
(Regex::new(r"\(\s*sin\s+").unwrap(), "(decimal-sin "),
|
|
||||||
(Regex::new(r"\(\s*cos\s+").unwrap(), "(decimal-cos "),
|
|
||||||
(Regex::new(r"\(\s*tan\s+").unwrap(), "(decimal-tan "),
|
|
||||||
|
|
||||||
// Comparison operators
|
|
||||||
(Regex::new(r"\(\s*>\s+").unwrap(), "(decimal-gt "),
|
|
||||||
(Regex::new(r"\(\s*<\s+").unwrap(), "(decimal-lt "),
|
|
||||||
(Regex::new(r"\(\s*=\s+").unwrap(), "(decimal-eq "),
|
|
||||||
(Regex::new(r"\(\s*>=\s+").unwrap(), "(decimal-gte "),
|
|
||||||
(Regex::new(r"\(\s*<=\s+").unwrap(), "(decimal-lte "),
|
|
||||||
|
|
||||||
// Utility functions
|
|
||||||
(Regex::new(r"\(\s*abs\s+").unwrap(), "(decimal-abs "),
|
|
||||||
(Regex::new(r"\(\s*min\s+").unwrap(), "(decimal-min "),
|
|
||||||
(Regex::new(r"\(\s*max\s+").unwrap(), "(decimal-max "),
|
|
||||||
(Regex::new(r"\(\s*round\s+").unwrap(), "(decimal-round "),
|
|
||||||
];
|
|
||||||
|
|
||||||
ScriptParser {
|
|
||||||
math_operators,
|
|
||||||
// VALID REGEX:
|
|
||||||
// This captures the preceding delimiter (group 1) and the number (group 2) separately.
|
|
||||||
// This avoids lookarounds and allows us to reconstruct the string correctly.
|
|
||||||
number_re: Regex::new(r"(^|[\s\(])(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)").unwrap(),
|
|
||||||
variable_re: Regex::new(r"\$([^\s)]+)").unwrap(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Transform a script by converting math operations and numbers to decimal functions
|
|
||||||
pub fn transform(&self, script: &str) -> String {
|
|
||||||
let mut transformed = script.to_string();
|
|
||||||
|
|
||||||
// CORRECT ORDER: Functions first, then variables, then numbers.
|
|
||||||
transformed = self.replace_math_functions(&transformed);
|
|
||||||
transformed = self.replace_variable_references(&transformed);
|
|
||||||
transformed = self.convert_numbers_to_strings(&transformed);
|
|
||||||
|
|
||||||
transformed
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Convert all unquoted numeric literals to quoted strings
|
|
||||||
fn convert_numbers_to_strings(&self, script: &str) -> String {
|
|
||||||
let parts: Vec<&str> = script.split('"').collect();
|
|
||||||
let mut result = String::new();
|
|
||||||
|
|
||||||
for (i, part) in parts.iter().enumerate() {
|
|
||||||
if i % 2 == 0 {
|
|
||||||
// Even indices are outside quotes - process them
|
|
||||||
let processed = self.number_re.replace_all(part, |caps: ®ex::Captures| {
|
|
||||||
// Reconstruct the string:
|
|
||||||
// caps[1] is the delimiter (space, '(', or start of string)
|
|
||||||
// caps[2] is the number itself
|
|
||||||
// We put the delimiter back and wrap the number in quotes.
|
|
||||||
format!("{}\"{}\"", &caps[1], &caps[2])
|
|
||||||
});
|
|
||||||
result.push_str(&processed);
|
|
||||||
} else {
|
|
||||||
// Odd indices are inside quotes - keep as is
|
|
||||||
result.push_str(part);
|
|
||||||
}
|
|
||||||
|
|
||||||
if i < parts.len() - 1 {
|
|
||||||
result.push('"');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
result
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Replace math function calls with decimal equivalents
|
|
||||||
fn replace_math_functions(&self, script: &str) -> String {
|
|
||||||
let mut result = script.to_string();
|
|
||||||
|
|
||||||
for (pattern, replacement) in &self.math_operators {
|
|
||||||
result = pattern.replace_all(&result, *replacement).to_string();
|
|
||||||
}
|
|
||||||
|
|
||||||
result
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Replace variable references ($var) with function calls
|
|
||||||
fn replace_variable_references(&self, script: &str) -> String {
|
|
||||||
self.variable_re.replace_all(script, |caps: ®ex::Captures| {
|
|
||||||
format!("(get-var \"{}\")", &caps[1])
|
|
||||||
}).to_string()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Extract dependencies from script (useful for analysis)
|
|
||||||
pub fn extract_dependencies(&self, script: &str) -> HashSet<String> {
|
|
||||||
let mut dependencies = HashSet::new();
|
|
||||||
|
|
||||||
for cap in self.variable_re.captures_iter(script) {
|
|
||||||
dependencies.insert(cap[1].to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for ScriptParser {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,240 +0,0 @@
|
|||||||
// src/registry.rs
|
|
||||||
use steel::steel_vm::engine::Engine;
|
|
||||||
use steel::steel_vm::register_fn::RegisterFn;
|
|
||||||
use crate::functions::*;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
pub struct FunctionRegistry;
|
|
||||||
|
|
||||||
impl FunctionRegistry {
|
|
||||||
/// Register all decimal math functions with the Steel VM
|
|
||||||
pub fn register_all(vm: &mut Engine) {
|
|
||||||
Self::register_basic_arithmetic(vm);
|
|
||||||
Self::register_precision_control(vm);
|
|
||||||
Self::register_advanced_math(vm);
|
|
||||||
Self::register_trigonometric(vm);
|
|
||||||
Self::register_comparison(vm);
|
|
||||||
Self::register_utility(vm);
|
|
||||||
Self::register_constants(vm);
|
|
||||||
Self::register_financial(vm);
|
|
||||||
Self::register_conversion(vm);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Register basic arithmetic functions
|
|
||||||
pub fn register_basic_arithmetic(vm: &mut Engine) {
|
|
||||||
vm.register_fn("decimal-add", decimal_add);
|
|
||||||
vm.register_fn("decimal-sub", decimal_sub);
|
|
||||||
vm.register_fn("decimal-mul", decimal_mul);
|
|
||||||
vm.register_fn("decimal-div", decimal_div);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Register precision control functions
|
|
||||||
pub fn register_precision_control(vm: &mut Engine) {
|
|
||||||
vm.register_fn("set-precision", set_precision);
|
|
||||||
vm.register_fn("get-precision", get_precision);
|
|
||||||
vm.register_fn("clear-precision", clear_precision);
|
|
||||||
vm.register_fn("decimal-format", decimal_format);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Register advanced mathematical functions
|
|
||||||
pub fn register_advanced_math(vm: &mut Engine) {
|
|
||||||
vm.register_fn("decimal-pow", decimal_pow);
|
|
||||||
vm.register_fn("decimal-sqrt", decimal_sqrt);
|
|
||||||
vm.register_fn("decimal-ln", decimal_ln);
|
|
||||||
vm.register_fn("decimal-log10", decimal_log10);
|
|
||||||
vm.register_fn("decimal-exp", decimal_exp);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Register trigonometric functions
|
|
||||||
pub fn register_trigonometric(vm: &mut Engine) {
|
|
||||||
vm.register_fn("decimal-sin", decimal_sin);
|
|
||||||
vm.register_fn("decimal-cos", decimal_cos);
|
|
||||||
vm.register_fn("decimal-tan", decimal_tan);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Register comparison functions
|
|
||||||
pub fn register_comparison(vm: &mut Engine) {
|
|
||||||
vm.register_fn("decimal-gt", decimal_gt);
|
|
||||||
vm.register_fn("decimal-gte", decimal_gte);
|
|
||||||
vm.register_fn("decimal-lt", decimal_lt);
|
|
||||||
vm.register_fn("decimal-lte", decimal_lte);
|
|
||||||
vm.register_fn("decimal-eq", decimal_eq);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Register utility functions
|
|
||||||
pub fn register_utility(vm: &mut Engine) {
|
|
||||||
vm.register_fn("decimal-abs", decimal_abs);
|
|
||||||
vm.register_fn("decimal-round", decimal_round);
|
|
||||||
vm.register_fn("decimal-min", decimal_min);
|
|
||||||
vm.register_fn("decimal-max", decimal_max);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Register mathematical constants
|
|
||||||
pub fn register_constants(vm: &mut Engine) {
|
|
||||||
vm.register_fn("decimal-zero", decimal_zero);
|
|
||||||
vm.register_fn("decimal-one", decimal_one);
|
|
||||||
vm.register_fn("decimal-pi", decimal_pi);
|
|
||||||
vm.register_fn("decimal-e", decimal_e);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Register financial functions
|
|
||||||
pub fn register_financial(vm: &mut Engine) {
|
|
||||||
vm.register_fn("decimal-percentage", decimal_percentage);
|
|
||||||
vm.register_fn("decimal-compound", decimal_compound);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Register type conversion functions
|
|
||||||
pub fn register_conversion(vm: &mut Engine) {
|
|
||||||
vm.register_fn("to-decimal", to_decimal);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Register variable access functions
|
|
||||||
pub fn register_variables(vm: &mut Engine, variables: HashMap<String, String>) {
|
|
||||||
let variables_for_get = variables.clone();
|
|
||||||
let variables_for_has = variables;
|
|
||||||
|
|
||||||
vm.register_fn("get-var", move |var_name: String| -> Result<String, String> {
|
|
||||||
variables_for_get.get(&var_name)
|
|
||||||
.cloned()
|
|
||||||
.ok_or_else(|| format!("Variable '{}' not found", var_name))
|
|
||||||
});
|
|
||||||
|
|
||||||
vm.register_fn("has-var?", move |var_name: String| -> bool {
|
|
||||||
variables_for_has.contains_key(&var_name)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get a list of all registered function names
|
|
||||||
pub fn get_function_names() -> Vec<&'static str> {
|
|
||||||
vec![
|
|
||||||
// Basic arithmetic
|
|
||||||
"decimal-add", "decimal-sub", "decimal-mul", "decimal-div",
|
|
||||||
// Advanced math
|
|
||||||
"decimal-pow", "decimal-sqrt", "decimal-ln", "decimal-log10", "decimal-exp",
|
|
||||||
// Trigonometric
|
|
||||||
"decimal-sin", "decimal-cos", "decimal-tan",
|
|
||||||
// Comparison
|
|
||||||
"decimal-gt", "decimal-gte", "decimal-lt", "decimal-lte", "decimal-eq",
|
|
||||||
// Utility
|
|
||||||
"decimal-abs", "decimal-round", "decimal-min", "decimal-max",
|
|
||||||
// Constants
|
|
||||||
"decimal-zero", "decimal-one", "decimal-pi", "decimal-e",
|
|
||||||
// Financial
|
|
||||||
"decimal-percentage", "decimal-compound",
|
|
||||||
// Conversion
|
|
||||||
"to-decimal",
|
|
||||||
// Variables
|
|
||||||
"get-var", "has-var?",
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Builder pattern for selective function registration
|
|
||||||
pub struct FunctionRegistryBuilder {
|
|
||||||
include_basic: bool,
|
|
||||||
include_advanced: bool,
|
|
||||||
include_trigonometric: bool,
|
|
||||||
include_comparison: bool,
|
|
||||||
include_utility: bool,
|
|
||||||
include_constants: bool,
|
|
||||||
include_financial: bool,
|
|
||||||
include_conversion: bool,
|
|
||||||
variables: Option<HashMap<String, String>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FunctionRegistryBuilder {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
include_basic: true,
|
|
||||||
include_advanced: true,
|
|
||||||
include_trigonometric: true,
|
|
||||||
include_comparison: true,
|
|
||||||
include_utility: true,
|
|
||||||
include_constants: true,
|
|
||||||
include_financial: true,
|
|
||||||
include_conversion: true,
|
|
||||||
variables: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn basic_arithmetic(mut self, include: bool) -> Self {
|
|
||||||
self.include_basic = include;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn advanced_math(mut self, include: bool) -> Self {
|
|
||||||
self.include_advanced = include;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn trigonometric(mut self, include: bool) -> Self {
|
|
||||||
self.include_trigonometric = include;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn comparison(mut self, include: bool) -> Self {
|
|
||||||
self.include_comparison = include;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn utility(mut self, include: bool) -> Self {
|
|
||||||
self.include_utility = include;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn constants(mut self, include: bool) -> Self {
|
|
||||||
self.include_constants = include;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn financial(mut self, include: bool) -> Self {
|
|
||||||
self.include_financial = include;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn conversion(mut self, include: bool) -> Self {
|
|
||||||
self.include_conversion = include;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_variables(mut self, variables: HashMap<String, String>) -> Self {
|
|
||||||
self.variables = Some(variables);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn register(self, vm: &mut Engine) {
|
|
||||||
if self.include_basic {
|
|
||||||
FunctionRegistry::register_basic_arithmetic(vm);
|
|
||||||
}
|
|
||||||
if self.include_advanced {
|
|
||||||
FunctionRegistry::register_advanced_math(vm);
|
|
||||||
}
|
|
||||||
if self.include_trigonometric {
|
|
||||||
FunctionRegistry::register_trigonometric(vm);
|
|
||||||
}
|
|
||||||
if self.include_comparison {
|
|
||||||
FunctionRegistry::register_comparison(vm);
|
|
||||||
}
|
|
||||||
if self.include_utility {
|
|
||||||
FunctionRegistry::register_utility(vm);
|
|
||||||
}
|
|
||||||
if self.include_constants {
|
|
||||||
FunctionRegistry::register_constants(vm);
|
|
||||||
}
|
|
||||||
if self.include_financial {
|
|
||||||
FunctionRegistry::register_financial(vm);
|
|
||||||
}
|
|
||||||
if self.include_conversion {
|
|
||||||
FunctionRegistry::register_conversion(vm);
|
|
||||||
}
|
|
||||||
if let Some(variables) = self.variables {
|
|
||||||
FunctionRegistry::register_variables(vm, variables);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for FunctionRegistryBuilder {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,160 +0,0 @@
|
|||||||
// src/utils.rs
|
|
||||||
use steel::rvals::SteelVal;
|
|
||||||
use rust_decimal::prelude::*;
|
|
||||||
use std::str::FromStr;
|
|
||||||
use thiserror::Error;
|
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
|
||||||
pub enum ConversionError {
|
|
||||||
#[error("Invalid decimal format: {0}")]
|
|
||||||
InvalidDecimal(String),
|
|
||||||
#[error("Unsupported SteelVal type: {0:?}")]
|
|
||||||
UnsupportedType(SteelVal),
|
|
||||||
#[error("Type conversion failed: {0}")]
|
|
||||||
ConversionFailed(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Utility functions for converting between Rust types and Steel values
|
|
||||||
pub struct TypeConverter;
|
|
||||||
|
|
||||||
impl TypeConverter {
|
|
||||||
/// Convert a SteelVal to a Decimal
|
|
||||||
pub fn steel_val_to_decimal(val: &SteelVal) -> Result<Decimal, ConversionError> {
|
|
||||||
match val {
|
|
||||||
SteelVal::StringV(s) => {
|
|
||||||
Decimal::from_str(&s.to_string())
|
|
||||||
.map_err(|e| ConversionError::InvalidDecimal(format!("{}: {}", s, e)))
|
|
||||||
}
|
|
||||||
SteelVal::NumV(n) => {
|
|
||||||
Decimal::try_from(*n)
|
|
||||||
.map_err(|e| ConversionError::InvalidDecimal(format!("{}: {}", n, e)))
|
|
||||||
}
|
|
||||||
SteelVal::IntV(i) => {
|
|
||||||
Ok(Decimal::from(*i))
|
|
||||||
}
|
|
||||||
_ => Err(ConversionError::UnsupportedType(val.clone()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Convert a Decimal to a SteelVal string
|
|
||||||
pub fn decimal_to_steel_val(decimal: Decimal) -> SteelVal {
|
|
||||||
SteelVal::StringV(decimal.to_string().into())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Convert multiple SteelVals to strings
|
|
||||||
pub fn steel_vals_to_strings(vals: Vec<SteelVal>) -> Result<Vec<String>, ConversionError> {
|
|
||||||
vals.into_iter()
|
|
||||||
.map(|val| Self::steel_val_to_string(val))
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Convert a single SteelVal to a string
|
|
||||||
pub fn steel_val_to_string(val: SteelVal) -> Result<String, ConversionError> {
|
|
||||||
match val {
|
|
||||||
SteelVal::StringV(s) => Ok(s.to_string()),
|
|
||||||
SteelVal::NumV(n) => Ok(n.to_string()),
|
|
||||||
SteelVal::IntV(i) => Ok(i.to_string()),
|
|
||||||
SteelVal::BoolV(b) => Ok(b.to_string()),
|
|
||||||
SteelVal::VectorV(v) => {
|
|
||||||
let string_values: Result<Vec<String>, _> = v.iter()
|
|
||||||
.map(|item| Self::steel_val_to_string(item.clone()))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
match string_values {
|
|
||||||
Ok(strings) => Ok(strings.join(",")),
|
|
||||||
Err(e) => Err(e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => Err(ConversionError::UnsupportedType(val))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Convert a string to a validated decimal string
|
|
||||||
pub fn validate_decimal_string(s: &str) -> Result<String, ConversionError> {
|
|
||||||
Decimal::from_str(s)
|
|
||||||
.map(|d| d.to_string())
|
|
||||||
.map_err(|e| ConversionError::InvalidDecimal(format!("{}: {}", s, e)))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Convert common Rust types to decimal strings
|
|
||||||
pub fn f64_to_decimal_string(f: f64) -> Result<String, ConversionError> {
|
|
||||||
Decimal::try_from(f)
|
|
||||||
.map(|d| d.to_string())
|
|
||||||
.map_err(|e| ConversionError::ConversionFailed(format!("f64 to decimal: {}", e)))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn i64_to_decimal_string(i: i64) -> String {
|
|
||||||
Decimal::from(i).to_string()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn u64_to_decimal_string(u: u64) -> String {
|
|
||||||
Decimal::from(u).to_string()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Utility functions for script validation and analysis
|
|
||||||
pub struct ScriptAnalyzer;
|
|
||||||
|
|
||||||
impl ScriptAnalyzer {
|
|
||||||
/// Check if a string looks like a valid decimal number
|
|
||||||
pub fn is_decimal_like(s: &str) -> bool {
|
|
||||||
Decimal::from_str(s.trim()).is_ok()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Extract all string literals from a script (simple regex-based approach)
|
|
||||||
pub fn extract_string_literals(script: &str) -> Vec<String> {
|
|
||||||
let re = regex::Regex::new(r#""((?:\\.|[^"])*)""#).unwrap();
|
|
||||||
re.captures_iter(script)
|
|
||||||
.map(|cap| cap[1].to_string())
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Count function calls in a script
|
|
||||||
pub fn count_function_calls(script: &str, function_name: &str) -> usize {
|
|
||||||
let pattern = format!(r"\({}\s+", regex::escape(function_name));
|
|
||||||
let re = regex::Regex::new(&pattern).unwrap();
|
|
||||||
re.find_iter(script).count()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if script contains any decimal functions
|
|
||||||
pub fn contains_decimal_functions(script: &str) -> bool {
|
|
||||||
let decimal_functions = [
|
|
||||||
"decimal-add", "decimal-sub", "decimal-mul", "decimal-div",
|
|
||||||
"decimal-pow", "decimal-sqrt", "decimal-ln", "decimal-log10", "decimal-exp",
|
|
||||||
"decimal-sin", "decimal-cos", "decimal-tan",
|
|
||||||
"decimal-gt", "decimal-gte", "decimal-lt", "decimal-lte", "decimal-eq",
|
|
||||||
"decimal-abs", "decimal-round", "decimal-min", "decimal-max",
|
|
||||||
];
|
|
||||||
|
|
||||||
decimal_functions.iter().any(|func| script.contains(func))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Utility functions for working with decimal precision
|
|
||||||
pub struct DecimalPrecision;
|
|
||||||
|
|
||||||
impl DecimalPrecision {
|
|
||||||
/// Set precision for a decimal string
|
|
||||||
pub fn set_precision(decimal_str: &str, precision: u32) -> Result<String, ConversionError> {
|
|
||||||
let decimal = Decimal::from_str(decimal_str)
|
|
||||||
.map_err(|e| ConversionError::InvalidDecimal(format!("{}: {}", decimal_str, e)))?;
|
|
||||||
|
|
||||||
Ok(decimal.round_dp(precision).to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the number of decimal places in a decimal string
|
|
||||||
pub fn get_decimal_places(decimal_str: &str) -> Result<u32, ConversionError> {
|
|
||||||
let decimal = Decimal::from_str(decimal_str)
|
|
||||||
.map_err(|e| ConversionError::InvalidDecimal(format!("{}: {}", decimal_str, e)))?;
|
|
||||||
|
|
||||||
Ok(decimal.scale())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Normalize decimal string (remove trailing zeros)
|
|
||||||
pub fn normalize(decimal_str: &str) -> Result<String, ConversionError> {
|
|
||||||
let decimal = Decimal::from_str(decimal_str)
|
|
||||||
.map_err(|e| ConversionError::InvalidDecimal(format!("{}: {}", decimal_str, e)))?;
|
|
||||||
|
|
||||||
Ok(decimal.normalize().to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,349 +0,0 @@
|
|||||||
// tests/boundary_tests.rs
|
|
||||||
use rstest::*;
|
|
||||||
use steel_decimal::*;
|
|
||||||
use rust_decimal::Decimal;
|
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
// Test extreme decimal values
|
|
||||||
#[rstest]
|
|
||||||
#[case("79228162514264337593543950335")] // Max decimal value
|
|
||||||
#[case("-79228162514264337593543950335")] // Min decimal value
|
|
||||||
#[case("0.0000000000000000000000000001")] // Smallest positive decimal (28 decimal places)
|
|
||||||
#[case("-0.0000000000000000000000000001")] // Smallest negative decimal
|
|
||||||
#[case("999999999999999999999999999.9999")] // Near maximum with precision
|
|
||||||
fn test_extreme_decimal_values(#[case] extreme_value: &str) {
|
|
||||||
// These should not panic, but may return errors for unsupported ranges
|
|
||||||
let add_result = decimal_add(extreme_value.to_string(), "1".to_string());
|
|
||||||
let sub_result = decimal_sub(extreme_value.to_string(), "1".to_string());
|
|
||||||
let abs_result = decimal_abs(extreme_value.to_string());
|
|
||||||
let conversion_result = to_decimal(extreme_value.to_string());
|
|
||||||
|
|
||||||
// At minimum, conversion should work for valid decimals
|
|
||||||
if let Ok(_) = Decimal::from_str(extreme_value) {
|
|
||||||
assert!(conversion_result.is_ok(), "Valid decimal should convert: {}", extreme_value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test maximum precision scenarios
|
|
||||||
#[rstest]
|
|
||||||
#[case(0)]
|
|
||||||
#[case(28)] // Maximum precision
|
|
||||||
fn test_precision_boundaries(#[case] precision: u32) {
|
|
||||||
let test_value = "123.456789012345678901234567890123456789";
|
|
||||||
|
|
||||||
if precision <= 28 {
|
|
||||||
let result = decimal_format(test_value.to_string(), precision);
|
|
||||||
assert!(result.is_ok(), "Precision {} should be valid", precision);
|
|
||||||
|
|
||||||
if let Ok(formatted) = result {
|
|
||||||
if precision == 0 {
|
|
||||||
assert!(!formatted.contains('.'), "Precision 0 should not have decimal point");
|
|
||||||
} else {
|
|
||||||
let decimal_places = formatted.split('.').nth(1).map(|s| s.len()).unwrap_or(0);
|
|
||||||
assert!(decimal_places <= precision as usize,
|
|
||||||
"Result should have at most {} decimal places, got {}",
|
|
||||||
precision, decimal_places);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test precision setting boundaries
|
|
||||||
#[rstest]
|
|
||||||
#[case(29)] // One over maximum
|
|
||||||
#[case(100)] // Way over maximum
|
|
||||||
#[case(u32::MAX)] // Maximum u32
|
|
||||||
fn test_invalid_precision_values(#[case] invalid_precision: u32) {
|
|
||||||
let result = set_precision(invalid_precision);
|
|
||||||
assert!(result.contains("Error"), "Should reject precision {}", invalid_precision);
|
|
||||||
|
|
||||||
// Verify precision wasn't actually set
|
|
||||||
let current = get_precision();
|
|
||||||
assert_ne!(current, invalid_precision.to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test very long input strings
|
|
||||||
#[rstest]
|
|
||||||
fn test_very_long_inputs() {
|
|
||||||
// Create very long but valid decimal string
|
|
||||||
let long_integer = "1".repeat(1000);
|
|
||||||
let long_decimal = format!("{}.{}", "1".repeat(500), "2".repeat(28)); // Respect max precision
|
|
||||||
let very_long_decimal = format!("{}.{}", "9".repeat(2000), "1".repeat(28));
|
|
||||||
|
|
||||||
// These might fail due to decimal limits, but shouldn't panic
|
|
||||||
let _ = to_decimal(long_integer);
|
|
||||||
let _ = to_decimal(long_decimal);
|
|
||||||
let _ = to_decimal(very_long_decimal);
|
|
||||||
|
|
||||||
// Operations on long strings
|
|
||||||
let _ = decimal_add("1".repeat(100), "2".repeat(100));
|
|
||||||
let _ = decimal_mul("1".repeat(50), "3".repeat(50));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test scientific notation boundaries
|
|
||||||
#[rstest]
|
|
||||||
#[case("1e308")] // Near f64 max
|
|
||||||
#[case("1e-324")] // Near f64 min
|
|
||||||
#[case("1e1000")] // Way beyond f64
|
|
||||||
#[case("1e-1000")] // Way beyond f64
|
|
||||||
#[case("1.5e100")]
|
|
||||||
#[case("9.999e99")]
|
|
||||||
#[case("1.23456789e-50")]
|
|
||||||
fn test_extreme_scientific_notation(#[case] sci_notation: &str) {
|
|
||||||
let result = to_decimal(sci_notation.to_string());
|
|
||||||
|
|
||||||
// Should either succeed or fail gracefully
|
|
||||||
match result {
|
|
||||||
Ok(converted) => {
|
|
||||||
// If successful, should be a valid decimal
|
|
||||||
assert!(Decimal::from_str(&converted).is_ok(),
|
|
||||||
"Converted result should be valid decimal: {}", converted);
|
|
||||||
}
|
|
||||||
Err(_) => {
|
|
||||||
// Failure is acceptable for extreme values
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test edge cases in arithmetic operations
|
|
||||||
#[rstest]
|
|
||||||
fn test_arithmetic_edge_cases() {
|
|
||||||
let max_decimal = "79228162514264337593543950335";
|
|
||||||
let min_decimal = "-79228162514264337593543950335";
|
|
||||||
let tiny_decimal = "0.0000000000000000000000000001";
|
|
||||||
|
|
||||||
// Addition near overflow - should return error, not panic
|
|
||||||
let add_result = decimal_add(max_decimal.to_string(), "1".to_string());
|
|
||||||
match add_result {
|
|
||||||
Ok(_) => {}, // Unlikely but possible
|
|
||||||
Err(e) => assert!(e.contains("overflow"), "Expected overflow error, got: {}", e),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Subtraction near underflow - should return error, not panic
|
|
||||||
let sub_result = decimal_sub(min_decimal.to_string(), "1".to_string());
|
|
||||||
match sub_result {
|
|
||||||
Ok(_) => {}, // Unlikely but possible
|
|
||||||
Err(e) => assert!(e.contains("overflow"), "Expected overflow error, got: {}", e),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Multiplication that could overflow - should return error, not panic
|
|
||||||
let mul_result = decimal_mul(max_decimal.to_string(), "2".to_string());
|
|
||||||
match mul_result {
|
|
||||||
Ok(_) => {}, // Unlikely but possible
|
|
||||||
Err(e) => assert!(e.contains("overflow"), "Expected overflow error, got: {}", e),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Division by very small number - might be very large but shouldn't panic
|
|
||||||
let div_result = decimal_div("1".to_string(), tiny_decimal.to_string());
|
|
||||||
match div_result {
|
|
||||||
Ok(_) => {}, // Should work
|
|
||||||
Err(e) => assert!(e.contains("overflow"), "Expected overflow error if any, got: {}", e),
|
|
||||||
}
|
|
||||||
|
|
||||||
// All operations should complete without panicking - if we get here, that's success!
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test malformed but potentially parseable inputs
|
|
||||||
#[rstest]
|
|
||||||
#[case("1.2.3")] // Multiple decimal points
|
|
||||||
#[case("1..2")] // Double decimal point
|
|
||||||
#[case("1.23e")] // Incomplete scientific notation
|
|
||||||
#[case("1.23e+")] // Incomplete positive exponent
|
|
||||||
#[case("1.23e-")] // Incomplete negative exponent
|
|
||||||
#[case("e5")] // Missing mantissa
|
|
||||||
#[case("1e1e1")] // Multiple exponents
|
|
||||||
#[case("++1")] // Multiple signs
|
|
||||||
#[case("--1")] // Multiple negative signs
|
|
||||||
#[case("1.23.45e6")] // Decimal in mantissa and base
|
|
||||||
fn test_malformed_decimal_inputs(#[case] malformed: &str) {
|
|
||||||
// These should all fail gracefully, not panic
|
|
||||||
let result = to_decimal(malformed.to_string());
|
|
||||||
assert!(result.is_err(), "Malformed input should be rejected: {}", malformed);
|
|
||||||
|
|
||||||
// Test in arithmetic operations too
|
|
||||||
let _ = decimal_add(malformed.to_string(), "1".to_string());
|
|
||||||
let _ = decimal_sub("1".to_string(), malformed.to_string());
|
|
||||||
let _ = decimal_mul(malformed.to_string(), "2".to_string());
|
|
||||||
let _ = decimal_abs(malformed.to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
#[case(".123")] // Leading decimal point - VALID in rust_decimal
|
|
||||||
#[case("123.")] // Trailing decimal point - VALID in rust_decimal
|
|
||||||
#[case("0.123")] // Standard format
|
|
||||||
#[case("123.0")] // Standard format with trailing zero
|
|
||||||
fn test_edge_case_valid_formats(#[case] valid_input: &str) {
|
|
||||||
// These should be accepted since rust_decimal accepts them
|
|
||||||
let result = to_decimal(valid_input.to_string());
|
|
||||||
assert!(result.is_ok(), "Valid rust_decimal format should be accepted: {}", valid_input);
|
|
||||||
|
|
||||||
// Should also work in arithmetic operations
|
|
||||||
let add_result = decimal_add(valid_input.to_string(), "1".to_string());
|
|
||||||
assert!(add_result.is_ok(), "Arithmetic should work with valid format: {}", valid_input);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test edge cases in comparison operations
|
|
||||||
#[rstest]
|
|
||||||
fn test_comparison_edge_cases() {
|
|
||||||
// Test comparisons at boundaries
|
|
||||||
let results = [
|
|
||||||
decimal_eq("0".to_string(), "-0".to_string()),
|
|
||||||
decimal_eq("0.0".to_string(), "0.00".to_string()),
|
|
||||||
decimal_gt("0.0000000000000000000000000001".to_string(), "0".to_string()),
|
|
||||||
decimal_lt("-0.0000000000000000000000000001".to_string(), "0".to_string()),
|
|
||||||
];
|
|
||||||
|
|
||||||
for result in results {
|
|
||||||
assert!(result.is_ok(), "Comparison should not fail");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test with extreme values
|
|
||||||
let max_val = "79228162514264337593543950335";
|
|
||||||
let min_val = "-79228162514264337593543950335";
|
|
||||||
|
|
||||||
assert!(decimal_gt(max_val.to_string(), min_val.to_string()).unwrap_or(false));
|
|
||||||
assert!(decimal_lt(min_val.to_string(), max_val.to_string()).unwrap_or(false));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test trigonometric functions at boundaries
|
|
||||||
#[rstest]
|
|
||||||
#[case("0")] // sin(0) = 0, cos(0) = 1
|
|
||||||
#[case("1.5707963267948966")] // π/2
|
|
||||||
#[case("3.1415926535897932")] // π
|
|
||||||
#[case("6.2831853071795865")] // 2π
|
|
||||||
#[case("100")] // Large angle
|
|
||||||
#[case("-100")] // Large negative angle
|
|
||||||
fn test_trig_function_boundaries(#[case] angle: &str) {
|
|
||||||
let sin_result = decimal_sin(angle.to_string());
|
|
||||||
let cos_result = decimal_cos(angle.to_string());
|
|
||||||
let tan_result = decimal_tan(angle.to_string());
|
|
||||||
|
|
||||||
// These should all complete without panicking
|
|
||||||
// Results may be imprecise for large angles, but should be finite
|
|
||||||
if let Ok(sin_val) = sin_result {
|
|
||||||
let sin_decimal = Decimal::from_str(&sin_val).unwrap();
|
|
||||||
assert!(sin_decimal.abs() <= Decimal::from(2), "Sin should be bounded: {}", sin_val);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Ok(cos_val) = cos_result {
|
|
||||||
let cos_decimal = Decimal::from_str(&cos_val).unwrap();
|
|
||||||
assert!(cos_decimal.abs() <= Decimal::from(2), "Cos should be bounded: {}", cos_val);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test logarithmic functions at boundaries
|
|
||||||
#[rstest]
|
|
||||||
#[case("1")] // ln(1) = 0
|
|
||||||
#[case("2.718281828459045")] // ln(e) = 1
|
|
||||||
#[case("0.0000000000000000000000000001")] // Very small positive
|
|
||||||
#[case("79228162514264337593543950335")] // Very large
|
|
||||||
fn test_log_function_boundaries(#[case] value: &str) {
|
|
||||||
let ln_result = decimal_ln(value.to_string());
|
|
||||||
let log10_result = decimal_log10(value.to_string());
|
|
||||||
|
|
||||||
// Should not panic, may return errors for invalid domains
|
|
||||||
if Decimal::from_str(value).unwrap() > Decimal::ZERO {
|
|
||||||
// Positive values should potentially work
|
|
||||||
match ln_result {
|
|
||||||
Ok(_) => {}, // Success is fine
|
|
||||||
Err(_) => {}, // Failure is also acceptable for extreme values
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Zero or negative should fail
|
|
||||||
assert!(ln_result.is_err(), "ln of non-positive should fail");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test square root at boundaries
|
|
||||||
#[rstest]
|
|
||||||
#[case("0")] // sqrt(0) = 0
|
|
||||||
#[case("1")] // sqrt(1) = 1
|
|
||||||
#[case("4")] // sqrt(4) = 2
|
|
||||||
#[case("0.0000000000000000000000000001")] // Very small
|
|
||||||
#[case("79228162514264337593543950335")] // Very large
|
|
||||||
fn test_sqrt_boundaries(#[case] value: &str) {
|
|
||||||
let result = decimal_sqrt(value.to_string());
|
|
||||||
|
|
||||||
if Decimal::from_str(value).unwrap() >= Decimal::ZERO {
|
|
||||||
match result {
|
|
||||||
Ok(sqrt_val) => {
|
|
||||||
let sqrt_decimal = Decimal::from_str(&sqrt_val).unwrap();
|
|
||||||
assert!(sqrt_decimal >= Decimal::ZERO, "Square root should be non-negative");
|
|
||||||
}
|
|
||||||
Err(_) => {
|
|
||||||
// May fail for very large values
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
assert!(result.is_err(), "Square root of negative should fail");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test power function boundaries
|
|
||||||
#[rstest]
|
|
||||||
#[case("2", "0")] // 2^0 = 1
|
|
||||||
#[case("2", "1")] // 2^1 = 2
|
|
||||||
#[case("2", "10")] // 2^10 = 1024
|
|
||||||
#[case("0", "5")] // 0^5 = 0
|
|
||||||
#[case("1", "1000")] // 1^1000 = 1
|
|
||||||
#[case("2", "100")] // Large exponent
|
|
||||||
#[case("10", "20")] // Another large case
|
|
||||||
fn test_pow_boundaries(#[case] base: &str, #[case] exponent: &str) {
|
|
||||||
let result = decimal_pow(base.to_string(), exponent.to_string());
|
|
||||||
|
|
||||||
// Should not panic, may overflow for large exponents
|
|
||||||
match &result {
|
|
||||||
Ok(_) => {}, // Success is fine
|
|
||||||
Err(_) => {}, // Overflow/underflow acceptable for extreme cases
|
|
||||||
}
|
|
||||||
|
|
||||||
// Special cases that should always work
|
|
||||||
if base == "1" {
|
|
||||||
// 1^anything = 1
|
|
||||||
if let Ok(ref val) = result {
|
|
||||||
assert_eq!(val, "1");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if exponent == "0" && base != "0" {
|
|
||||||
// anything^0 = 1 (except 0^0 which is undefined)
|
|
||||||
if let Ok(ref val) = result {
|
|
||||||
assert_eq!(val, "1");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test financial functions with boundary values
|
|
||||||
#[rstest]
|
|
||||||
fn test_financial_boundaries() {
|
|
||||||
// Test percentage calculations
|
|
||||||
let percentage_tests = [
|
|
||||||
("0", "50"), // 0% of 50
|
|
||||||
("100", "0"), // 100% of 0
|
|
||||||
("100", "100"), // 100% of 100
|
|
||||||
("1000000", "0.001"), // Large amount, tiny percentage
|
|
||||||
("0.001", "1000000"), // Tiny amount, huge percentage
|
|
||||||
];
|
|
||||||
|
|
||||||
for (amount, percentage) in percentage_tests {
|
|
||||||
let result = decimal_percentage(amount.to_string(), percentage.to_string());
|
|
||||||
assert!(result.is_ok(), "Percentage calculation should work: {}% of {}", percentage, amount);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test compound interest edge cases
|
|
||||||
let compound_tests = [
|
|
||||||
("1000", "0", "10"), // 0% interest
|
|
||||||
("1000", "0.05", "0"), // 0 time periods
|
|
||||||
("0", "0.05", "10"), // 0 principal
|
|
||||||
("1", "2", "10"), // 200% interest (extreme but valid)
|
|
||||||
];
|
|
||||||
|
|
||||||
for (principal, rate, time) in compound_tests {
|
|
||||||
let result = decimal_compound(principal.to_string(), rate.to_string(), time.to_string());
|
|
||||||
// Some extreme cases may overflow, but shouldn't panic
|
|
||||||
match result {
|
|
||||||
Ok(_) => {},
|
|
||||||
Err(_) => {}, // Acceptable for extreme cases
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,478 +0,0 @@
|
|||||||
// tests/concurrency_tests.rs
|
|
||||||
use steel_decimal::*;
|
|
||||||
use std::sync::{Arc, Barrier, Mutex};
|
|
||||||
use std::thread;
|
|
||||||
use std::time::Duration;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
// Test precision isolation between threads
|
|
||||||
#[test]
|
|
||||||
fn test_precision_thread_isolation() {
|
|
||||||
let num_threads = 10;
|
|
||||||
let barrier = Arc::new(Barrier::new(num_threads));
|
|
||||||
let results = Arc::new(Mutex::new(Vec::new()));
|
|
||||||
|
|
||||||
let handles: Vec<_> = (0..num_threads)
|
|
||||||
.map(|thread_id| {
|
|
||||||
let barrier = barrier.clone();
|
|
||||||
let results = results.clone();
|
|
||||||
|
|
||||||
thread::spawn(move || {
|
|
||||||
// Each thread sets different precision
|
|
||||||
let precision = thread_id as u32 % 5; // 0-4
|
|
||||||
set_precision(precision);
|
|
||||||
|
|
||||||
// Wait for all threads to set their precision
|
|
||||||
barrier.wait();
|
|
||||||
|
|
||||||
// Perform calculation
|
|
||||||
let result = decimal_add("1.123456789".to_string(), "2.987654321".to_string()).unwrap();
|
|
||||||
|
|
||||||
// Verify precision is maintained in this thread
|
|
||||||
let current_precision = get_precision();
|
|
||||||
|
|
||||||
results.lock().unwrap().push((thread_id, precision, result, current_precision));
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
for handle in handles {
|
|
||||||
handle.join().unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
let results = results.lock().unwrap();
|
|
||||||
|
|
||||||
// Verify each thread maintained its own precision
|
|
||||||
for (thread_id, set_precision, result, current_precision) in results.iter() {
|
|
||||||
assert_eq!(current_precision, &set_precision.to_string(),
|
|
||||||
"Thread {} precision not isolated", thread_id);
|
|
||||||
|
|
||||||
// Verify result respects the precision
|
|
||||||
if *set_precision > 0 {
|
|
||||||
let decimal_places = result.split('.').nth(1).map(|s| s.len()).unwrap_or(0);
|
|
||||||
assert!(decimal_places <= *set_precision as usize,
|
|
||||||
"Thread {} result {} has more than {} decimal places",
|
|
||||||
thread_id, result, set_precision);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test concurrent arithmetic operations
|
|
||||||
#[test]
|
|
||||||
fn test_concurrent_arithmetic_operations() {
|
|
||||||
let num_threads = 20;
|
|
||||||
let operations_per_thread = 100;
|
|
||||||
let barrier = Arc::new(Barrier::new(num_threads));
|
|
||||||
let errors = Arc::new(Mutex::new(Vec::new()));
|
|
||||||
|
|
||||||
let handles: Vec<_> = (0..num_threads)
|
|
||||||
.map(|thread_id| {
|
|
||||||
let barrier = barrier.clone();
|
|
||||||
let errors = errors.clone();
|
|
||||||
|
|
||||||
thread::spawn(move || {
|
|
||||||
barrier.wait();
|
|
||||||
|
|
||||||
for i in 0..operations_per_thread {
|
|
||||||
let a = format!("{}.{}", thread_id, i);
|
|
||||||
let b = format!("{}.{}", i, thread_id);
|
|
||||||
|
|
||||||
// Test various operations don't interfere
|
|
||||||
let add_result = decimal_add(a.clone(), b.clone());
|
|
||||||
let mul_result = decimal_mul(a.clone(), b.clone());
|
|
||||||
let sub_result = decimal_sub(a.clone(), b.clone());
|
|
||||||
|
|
||||||
if add_result.is_err() || mul_result.is_err() || sub_result.is_err() {
|
|
||||||
errors.lock().unwrap().push(format!(
|
|
||||||
"Thread {}, iteration {}: arithmetic error",
|
|
||||||
thread_id, i
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
for handle in handles {
|
|
||||||
handle.join().unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
let errors = errors.lock().unwrap();
|
|
||||||
assert!(errors.is_empty(), "Concurrent arithmetic errors: {:?}", *errors);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test Steel VM registration under concurrent load
|
|
||||||
#[test]
|
|
||||||
fn test_concurrent_vm_registration() {
|
|
||||||
use steel::steel_vm::engine::Engine;
|
|
||||||
|
|
||||||
let num_threads = 5;
|
|
||||||
let barrier = Arc::new(Barrier::new(num_threads));
|
|
||||||
let errors = Arc::new(Mutex::new(Vec::new()));
|
|
||||||
|
|
||||||
let handles: Vec<_> = (0..num_threads)
|
|
||||||
.map(|thread_id| {
|
|
||||||
let barrier = barrier.clone();
|
|
||||||
let errors = errors.clone();
|
|
||||||
|
|
||||||
thread::spawn(move || {
|
|
||||||
barrier.wait();
|
|
||||||
|
|
||||||
// Each thread creates its own VM and registers functions
|
|
||||||
let mut vm = Engine::new();
|
|
||||||
FunctionRegistry::register_all(&mut vm);
|
|
||||||
|
|
||||||
// Test execution
|
|
||||||
let script = r#"(decimal-add "1.5" "2.3")"#;
|
|
||||||
let result = vm.compile_and_run_raw_program(script.to_string());
|
|
||||||
|
|
||||||
match result {
|
|
||||||
Ok(vals) => {
|
|
||||||
if vals.len() != 1 {
|
|
||||||
errors.lock().unwrap().push(format!(
|
|
||||||
"Thread {}: Wrong number of results", thread_id
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
errors.lock().unwrap().push(format!(
|
|
||||||
"Thread {}: VM execution error: {}", thread_id, e
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
for handle in handles {
|
|
||||||
handle.join().unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
let errors = errors.lock().unwrap();
|
|
||||||
assert!(errors.is_empty(), "Concurrent VM errors: {:?}", *errors);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test variable access concurrency
|
|
||||||
#[test]
|
|
||||||
fn test_concurrent_variable_access() {
|
|
||||||
use steel::steel_vm::engine::Engine;
|
|
||||||
|
|
||||||
let num_threads = 8;
|
|
||||||
let barrier = Arc::new(Barrier::new(num_threads));
|
|
||||||
let errors = Arc::new(Mutex::new(Vec::new()));
|
|
||||||
|
|
||||||
let handles: Vec<_> = (0..num_threads)
|
|
||||||
.map(|thread_id| {
|
|
||||||
let barrier = barrier.clone();
|
|
||||||
let errors = errors.clone();
|
|
||||||
|
|
||||||
thread::spawn(move || {
|
|
||||||
// Each thread has its own variable set
|
|
||||||
let mut variables = HashMap::new();
|
|
||||||
variables.insert(format!("var_{}", thread_id), format!("{}.0", thread_id * 10));
|
|
||||||
variables.insert("shared".to_string(), "42.0".to_string());
|
|
||||||
|
|
||||||
let mut vm = Engine::new();
|
|
||||||
FunctionRegistry::register_variables(&mut vm, variables);
|
|
||||||
|
|
||||||
barrier.wait();
|
|
||||||
|
|
||||||
// Test variable access
|
|
||||||
let get_script = format!(r#"(get-var "var_{}")"#, thread_id);
|
|
||||||
let has_script = format!(r#"(has-var? "var_{}")"#, thread_id);
|
|
||||||
let shared_script = r#"(get-var "shared")"#.to_string();
|
|
||||||
|
|
||||||
for script in [get_script, shared_script] {
|
|
||||||
match vm.compile_and_run_raw_program(script) {
|
|
||||||
Ok(_) => {}
|
|
||||||
Err(e) => {
|
|
||||||
errors.lock().unwrap().push(format!(
|
|
||||||
"Thread {}: Variable access error: {}", thread_id, e
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
match vm.compile_and_run_raw_program(has_script) {
|
|
||||||
Ok(_) => {}
|
|
||||||
Err(e) => {
|
|
||||||
errors.lock().unwrap().push(format!(
|
|
||||||
"Thread {}: Variable check error: {}", thread_id, e
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
for handle in handles {
|
|
||||||
handle.join().unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
let errors = errors.lock().unwrap();
|
|
||||||
assert!(errors.is_empty(), "Concurrent variable access errors: {:?}", *errors);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test precision state under rapid changes
|
|
||||||
#[test]
|
|
||||||
fn test_rapid_precision_changes() {
|
|
||||||
let num_threads = 4;
|
|
||||||
let changes_per_thread = 1000;
|
|
||||||
let barrier = Arc::new(Barrier::new(num_threads));
|
|
||||||
let inconsistencies = Arc::new(Mutex::new(0));
|
|
||||||
|
|
||||||
let handles: Vec<_> = (0..num_threads)
|
|
||||||
.map(|_thread_id| {
|
|
||||||
let barrier = barrier.clone();
|
|
||||||
let inconsistencies = inconsistencies.clone();
|
|
||||||
|
|
||||||
thread::spawn(move || {
|
|
||||||
barrier.wait();
|
|
||||||
|
|
||||||
for i in 0..changes_per_thread {
|
|
||||||
let precision = (i % 5) as u32; // Cycle through 0-4
|
|
||||||
|
|
||||||
set_precision(precision);
|
|
||||||
|
|
||||||
// Immediately check precision
|
|
||||||
let current = get_precision();
|
|
||||||
if current != precision.to_string() {
|
|
||||||
*inconsistencies.lock().unwrap() += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Perform calculation and verify
|
|
||||||
let result = decimal_add("1.123456".to_string(), "2.654321".to_string()).unwrap();
|
|
||||||
|
|
||||||
if precision > 0 {
|
|
||||||
let decimal_places = result.split('.').nth(1).map(|s| s.len()).unwrap_or(0);
|
|
||||||
if decimal_places > precision as usize {
|
|
||||||
*inconsistencies.lock().unwrap() += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
for handle in handles {
|
|
||||||
handle.join().unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
let inconsistencies = *inconsistencies.lock().unwrap();
|
|
||||||
assert_eq!(inconsistencies, 0, "Found {} precision inconsistencies", inconsistencies);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test parser thread safety
|
|
||||||
#[test]
|
|
||||||
fn test_parser_thread_safety() {
|
|
||||||
let num_threads = 10;
|
|
||||||
let transformations_per_thread = 100;
|
|
||||||
let barrier = Arc::new(Barrier::new(num_threads));
|
|
||||||
let errors = Arc::new(Mutex::new(Vec::new()));
|
|
||||||
|
|
||||||
let test_scripts = vec![
|
|
||||||
"(+ 1.5 2.3)",
|
|
||||||
"(* $x $y)",
|
|
||||||
"(sqrt (+ (* $a $a) (* $b $b)))",
|
|
||||||
"(/ (- $max $min) 2)",
|
|
||||||
"(abs (- $value $target))",
|
|
||||||
];
|
|
||||||
|
|
||||||
let handles: Vec<_> = (0..num_threads)
|
|
||||||
.map(|thread_id| {
|
|
||||||
let barrier = barrier.clone();
|
|
||||||
let errors = errors.clone();
|
|
||||||
let scripts = test_scripts.clone();
|
|
||||||
|
|
||||||
thread::spawn(move || {
|
|
||||||
let parser = ScriptParser::new();
|
|
||||||
barrier.wait();
|
|
||||||
|
|
||||||
for i in 0..transformations_per_thread {
|
|
||||||
let script = &scripts[i % scripts.len()];
|
|
||||||
|
|
||||||
let transformed = parser.transform(script);
|
|
||||||
let _dependencies = parser.extract_dependencies(script);
|
|
||||||
|
|
||||||
// Basic validation
|
|
||||||
let open_count = transformed.chars().filter(|c| *c == '(').count();
|
|
||||||
let close_count = transformed.chars().filter(|c| *c == ')').count();
|
|
||||||
|
|
||||||
if open_count != close_count {
|
|
||||||
errors.lock().unwrap().push(format!(
|
|
||||||
"Thread {}, iteration {}: Unbalanced parentheses in {}",
|
|
||||||
thread_id, i, transformed
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
if !transformed.contains("decimal-") && script.contains('+') {
|
|
||||||
errors.lock().unwrap().push(format!(
|
|
||||||
"Thread {}, iteration {}: Transformation failed for {}",
|
|
||||||
thread_id, i, script
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
for handle in handles {
|
|
||||||
handle.join().unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
let errors = errors.lock().unwrap();
|
|
||||||
assert!(errors.is_empty(), "Parser thread safety errors: {:?}", *errors);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test memory safety under concurrent load
|
|
||||||
#[test]
|
|
||||||
fn test_memory_safety_concurrent_load() {
|
|
||||||
let num_threads = 8;
|
|
||||||
let iterations = 500;
|
|
||||||
let barrier = Arc::new(Barrier::new(num_threads));
|
|
||||||
|
|
||||||
let handles: Vec<_> = (0..num_threads)
|
|
||||||
.map(|thread_id| {
|
|
||||||
let barrier = barrier.clone();
|
|
||||||
|
|
||||||
thread::spawn(move || {
|
|
||||||
barrier.wait();
|
|
||||||
|
|
||||||
// Create many SteelDecimal instances
|
|
||||||
for i in 0..iterations {
|
|
||||||
let mut steel_decimal = SteelDecimal::new();
|
|
||||||
|
|
||||||
// Add variables
|
|
||||||
steel_decimal.add_variable(format!("var_{}", i), format!("{}.{}", thread_id, i));
|
|
||||||
|
|
||||||
// Transform scripts
|
|
||||||
let script = format!("(+ {} {})", i, thread_id);
|
|
||||||
let _ = steel_decimal.transform(&script);
|
|
||||||
|
|
||||||
// Extract dependencies
|
|
||||||
let _ = steel_decimal.extract_dependencies(&script);
|
|
||||||
|
|
||||||
// Small delay to increase chance of race conditions
|
|
||||||
if i % 100 == 0 {
|
|
||||||
thread::sleep(Duration::from_micros(1));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
for handle in handles {
|
|
||||||
handle.join().unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we get here without panicking, memory safety is maintained
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test precision cleanup after thread termination
|
|
||||||
#[test]
|
|
||||||
fn test_precision_cleanup_after_thread_death() {
|
|
||||||
// Create thread that sets precision and dies
|
|
||||||
let handle = thread::spawn(|| {
|
|
||||||
set_precision(3);
|
|
||||||
decimal_add("1.123456".to_string(), "2.654321".to_string()).unwrap()
|
|
||||||
});
|
|
||||||
|
|
||||||
let result = handle.join().unwrap();
|
|
||||||
|
|
||||||
// Verify the result had the precision applied
|
|
||||||
let decimal_places = result.split('.').nth(1).map(|s| s.len()).unwrap_or(0);
|
|
||||||
assert!(decimal_places <= 3);
|
|
||||||
|
|
||||||
// In main thread, precision should be unaffected
|
|
||||||
let main_precision = get_precision();
|
|
||||||
// Should be "full" (default) since we haven't set it in main thread
|
|
||||||
assert_eq!(main_precision, "full");
|
|
||||||
|
|
||||||
// Create another thread - should start fresh
|
|
||||||
let handle2 = thread::spawn(|| {
|
|
||||||
let precision = get_precision();
|
|
||||||
(precision, decimal_add("1.123456".to_string(), "2.654321".to_string()).unwrap())
|
|
||||||
});
|
|
||||||
|
|
||||||
let (new_precision, new_result) = handle2.join().unwrap();
|
|
||||||
assert_eq!(new_precision, "full");
|
|
||||||
|
|
||||||
// This result should use full precision
|
|
||||||
let new_decimal_places = new_result.split('.').nth(1).map(|s| s.len()).unwrap_or(0);
|
|
||||||
assert!(new_decimal_places > 3); // Should be more than the previous thread's precision
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stress test with mixed operations
|
|
||||||
#[test]
|
|
||||||
fn test_concurrent_stress_mixed_operations() {
|
|
||||||
let num_threads = 6;
|
|
||||||
let operations_per_thread = 200;
|
|
||||||
let barrier = Arc::new(Barrier::new(num_threads));
|
|
||||||
let total_errors = Arc::new(Mutex::new(0));
|
|
||||||
|
|
||||||
let handles: Vec<_> = (0..num_threads)
|
|
||||||
.map(|thread_id| {
|
|
||||||
let barrier = barrier.clone();
|
|
||||||
let total_errors = total_errors.clone();
|
|
||||||
|
|
||||||
thread::spawn(move || {
|
|
||||||
let mut errors = 0;
|
|
||||||
barrier.wait();
|
|
||||||
|
|
||||||
for i in 0..operations_per_thread {
|
|
||||||
// Mix of precision settings
|
|
||||||
if i % 50 == 0 {
|
|
||||||
set_precision((thread_id as u32) % 5);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mix of operations
|
|
||||||
match i % 6 {
|
|
||||||
0 => {
|
|
||||||
if decimal_add(format!("{}.{}", thread_id, i), "1.0".to_string()).is_err() {
|
|
||||||
errors += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
1 => {
|
|
||||||
if decimal_mul(format!("{}", i), format!("{}.5", thread_id)).is_err() {
|
|
||||||
errors += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
2 => {
|
|
||||||
if decimal_sqrt(format!("{}", i + 1)).is_err() && i > 0 {
|
|
||||||
errors += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
3 => {
|
|
||||||
if decimal_abs(format!("-{}.{}", thread_id, i)).is_err() {
|
|
||||||
errors += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
4 => {
|
|
||||||
if decimal_gt(format!("{}", i), format!("{}", thread_id)).is_err() {
|
|
||||||
errors += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
5 => {
|
|
||||||
if to_decimal(format!("{}.{}e1", thread_id, i)).is_err() {
|
|
||||||
errors += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => unreachable!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
*total_errors.lock().unwrap() += errors;
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
for handle in handles {
|
|
||||||
handle.join().unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
let total_errors = *total_errors.lock().unwrap();
|
|
||||||
|
|
||||||
// Allow some errors for edge cases (like sqrt of 0), but not too many
|
|
||||||
assert!(total_errors < num_threads * operations_per_thread / 10,
|
|
||||||
"Too many errors in stress test: {}", total_errors);
|
|
||||||
}
|
|
||||||
@@ -1,341 +0,0 @@
|
|||||||
use rstest::*;
|
|
||||||
use steel_decimal::*;
|
|
||||||
|
|
||||||
// Basic Arithmetic Tests
|
|
||||||
#[rstest]
|
|
||||||
#[case("1.5", "2.3", "3.8")]
|
|
||||||
#[case("10", "5", "15")]
|
|
||||||
#[case("-5.5", "3.2", "-2.3")]
|
|
||||||
#[case("0", "42", "42")]
|
|
||||||
fn test_decimal_add(#[case] a: &str, #[case] b: &str, #[case] expected: &str) {
|
|
||||||
let result = decimal_add(a.to_string(), b.to_string()).unwrap();
|
|
||||||
assert_eq!(result, expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
#[case("10", "3", "7")]
|
|
||||||
#[case("5.5", "2.2", "3.3")]
|
|
||||||
#[case("0", "5", "-5")]
|
|
||||||
#[case("-3", "-2", "-1")]
|
|
||||||
fn test_decimal_sub(#[case] a: &str, #[case] b: &str, #[case] expected: &str) {
|
|
||||||
let result = decimal_sub(a.to_string(), b.to_string()).unwrap();
|
|
||||||
assert_eq!(result, expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
#[case("2", "3", "6")]
|
|
||||||
#[case("2.5", "4", "10.0")] // rust_decimal preserves precision
|
|
||||||
#[case("-2", "3", "-6")]
|
|
||||||
#[case("0", "100", "0")]
|
|
||||||
fn test_decimal_mul(#[case] a: &str, #[case] b: &str, #[case] expected: &str) {
|
|
||||||
let result = decimal_mul(a.to_string(), b.to_string()).unwrap();
|
|
||||||
assert_eq!(result, expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
#[case("10", "2", "5")]
|
|
||||||
#[case("15", "3", "5")]
|
|
||||||
#[case("7.5", "2.5", "3")]
|
|
||||||
fn test_decimal_div(#[case] a: &str, #[case] b: &str, #[case] expected: &str) {
|
|
||||||
let result = decimal_div(a.to_string(), b.to_string()).unwrap();
|
|
||||||
assert_eq!(result, expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
fn test_decimal_div_by_zero() {
|
|
||||||
let result = decimal_div("10".to_string(), "0".to_string());
|
|
||||||
assert!(result.is_err());
|
|
||||||
assert!(result.unwrap_err().contains("Division by zero"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Advanced Math Tests
|
|
||||||
#[rstest]
|
|
||||||
#[case("2", "3", "8")]
|
|
||||||
#[case("5", "2", "25")]
|
|
||||||
#[case("10", "0", "1")]
|
|
||||||
fn test_decimal_pow(#[case] base: &str, #[case] exp: &str, #[case] expected: &str) {
|
|
||||||
let result = decimal_pow(base.to_string(), exp.to_string()).unwrap();
|
|
||||||
assert_eq!(result, expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
#[case("16")]
|
|
||||||
#[case("25")]
|
|
||||||
#[case("9")]
|
|
||||||
fn test_decimal_sqrt(#[case] input: &str) {
|
|
||||||
let result = decimal_sqrt(input.to_string()).unwrap();
|
|
||||||
// rust_decimal sqrt returns high precision - just verify it starts with the right digit
|
|
||||||
match input {
|
|
||||||
"16" => assert!(result.starts_with("4")),
|
|
||||||
"25" => assert!(result.starts_with("5")),
|
|
||||||
"9" => assert!(result.starts_with("3")),
|
|
||||||
_ => panic!("Unexpected input"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
fn test_decimal_sqrt_negative() {
|
|
||||||
let result = decimal_sqrt("-4".to_string());
|
|
||||||
assert!(result.is_err());
|
|
||||||
assert!(result.unwrap_err().contains("Square root failed"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Comparison Tests
|
|
||||||
#[rstest]
|
|
||||||
#[case("5", "3", true)]
|
|
||||||
#[case("3", "5", false)]
|
|
||||||
#[case("5", "5", false)]
|
|
||||||
fn test_decimal_gt(#[case] a: &str, #[case] b: &str, #[case] expected: bool) {
|
|
||||||
let result = decimal_gt(a.to_string(), b.to_string()).unwrap();
|
|
||||||
assert_eq!(result, expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
#[case("3", "5", true)]
|
|
||||||
#[case("5", "3", false)]
|
|
||||||
#[case("5", "5", false)]
|
|
||||||
fn test_decimal_lt(#[case] a: &str, #[case] b: &str, #[case] expected: bool) {
|
|
||||||
let result = decimal_lt(a.to_string(), b.to_string()).unwrap();
|
|
||||||
assert_eq!(result, expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
#[case("5", "5", true)]
|
|
||||||
#[case("5", "3", false)]
|
|
||||||
#[case("3.14", "3.14", true)]
|
|
||||||
fn test_decimal_eq(#[case] a: &str, #[case] b: &str, #[case] expected: bool) {
|
|
||||||
let result = decimal_eq(a.to_string(), b.to_string()).unwrap();
|
|
||||||
assert_eq!(result, expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
#[case("5", "3", true)]
|
|
||||||
#[case("3", "5", false)]
|
|
||||||
#[case("5", "5", true)]
|
|
||||||
fn test_decimal_gte(#[case] a: &str, #[case] b: &str, #[case] expected: bool) {
|
|
||||||
let result = decimal_gte(a.to_string(), b.to_string()).unwrap();
|
|
||||||
assert_eq!(result, expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
#[case("3", "5", true)]
|
|
||||||
#[case("5", "3", false)]
|
|
||||||
#[case("5", "5", true)]
|
|
||||||
fn test_decimal_lte(#[case] a: &str, #[case] b: &str, #[case] expected: bool) {
|
|
||||||
let result = decimal_lte(a.to_string(), b.to_string()).unwrap();
|
|
||||||
assert_eq!(result, expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Utility Tests
|
|
||||||
#[rstest]
|
|
||||||
#[case("-5", "5")]
|
|
||||||
#[case("3.14", "3.14")]
|
|
||||||
#[case("-0", "0")]
|
|
||||||
fn test_decimal_abs(#[case] input: &str, #[case] expected: &str) {
|
|
||||||
let result = decimal_abs(input.to_string()).unwrap();
|
|
||||||
assert_eq!(result, expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
#[case("3", "5", "3")]
|
|
||||||
#[case("10", "7", "7")]
|
|
||||||
#[case("-5", "-2", "-5")]
|
|
||||||
fn test_decimal_min(#[case] a: &str, #[case] b: &str, #[case] expected: &str) {
|
|
||||||
let result = decimal_min(a.to_string(), b.to_string()).unwrap();
|
|
||||||
assert_eq!(result, expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
#[case("3", "5", "5")]
|
|
||||||
#[case("10", "7", "10")]
|
|
||||||
#[case("-5", "-2", "-2")]
|
|
||||||
fn test_decimal_max(#[case] a: &str, #[case] b: &str, #[case] expected: &str) {
|
|
||||||
let result = decimal_max(a.to_string(), b.to_string()).unwrap();
|
|
||||||
assert_eq!(result, expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
#[case("3.14159", 2, "3.14")]
|
|
||||||
#[case("2.71828", 3, "2.718")]
|
|
||||||
#[case("10.999", 0, "11")]
|
|
||||||
fn test_decimal_round(#[case] input: &str, #[case] places: i32, #[case] expected: &str) {
|
|
||||||
let result = decimal_round(input.to_string(), places).unwrap();
|
|
||||||
assert_eq!(result, expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Constants Tests
|
|
||||||
#[rstest]
|
|
||||||
fn test_decimal_constants() {
|
|
||||||
assert_eq!(decimal_zero(), "0");
|
|
||||||
assert_eq!(decimal_one(), "1");
|
|
||||||
assert!(decimal_pi().starts_with("3.14159"));
|
|
||||||
assert!(decimal_e().starts_with("2.71828"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Financial Functions Tests
|
|
||||||
#[rstest]
|
|
||||||
#[case("100", "15", "15")]
|
|
||||||
#[case("1000", "5.5", "55.0")] // rust_decimal preserves precision from 5.5
|
|
||||||
#[case("250", "20", "50")]
|
|
||||||
fn test_decimal_percentage(#[case] amount: &str, #[case] percentage: &str, #[case] expected: &str) {
|
|
||||||
let result = decimal_percentage(amount.to_string(), percentage.to_string()).unwrap();
|
|
||||||
assert_eq!(result, expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
#[case("1000", "0.05", "1", "1050.00")] // rust_decimal preserves precision from 0.05
|
|
||||||
#[case("1000", "0.1", "2", "1210.00")] // rust_decimal preserves precision from 0.1
|
|
||||||
fn test_decimal_compound(#[case] principal: &str, #[case] rate: &str, #[case] time: &str, #[case] expected: &str) {
|
|
||||||
let result = decimal_compound(principal.to_string(), rate.to_string(), time.to_string()).unwrap();
|
|
||||||
assert_eq!(result, expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Type Conversion Tests
|
|
||||||
#[rstest]
|
|
||||||
#[case("123.456", "123.456")]
|
|
||||||
#[case("42", "42")]
|
|
||||||
#[case("0.001", "0.001")]
|
|
||||||
fn test_to_decimal(#[case] input: &str, #[case] expected: &str) {
|
|
||||||
let result = to_decimal(input.to_string()).unwrap();
|
|
||||||
assert_eq!(result, expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
fn test_to_decimal_invalid() {
|
|
||||||
let result = to_decimal("not_a_number".to_string());
|
|
||||||
assert!(result.is_err());
|
|
||||||
assert!(result.unwrap_err().contains("Invalid decimal"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Error Handling Tests
|
|
||||||
#[rstest]
|
|
||||||
#[case("invalid", "2")]
|
|
||||||
#[case("1", "invalid")]
|
|
||||||
#[case("abc", "def")]
|
|
||||||
fn test_decimal_add_invalid_input(#[case] a: &str, #[case] b: &str) {
|
|
||||||
let result = decimal_add(a.to_string(), b.to_string());
|
|
||||||
assert!(result.is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
#[case("invalid")]
|
|
||||||
#[case("not_a_number")]
|
|
||||||
#[case("abc")]
|
|
||||||
fn test_decimal_sqrt_invalid_input(#[case] input: &str) {
|
|
||||||
let result = decimal_sqrt(input.to_string());
|
|
||||||
assert!(result.is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Edge Cases
|
|
||||||
#[rstest]
|
|
||||||
#[case("0.000000001", "0.000000001", "0.000000002")]
|
|
||||||
#[case("999999999999999", "1", "1000000000000000")]
|
|
||||||
fn test_decimal_precision_edge_cases(#[case] a: &str, #[case] b: &str, #[case] expected: &str) {
|
|
||||||
let result = decimal_add(a.to_string(), b.to_string()).unwrap();
|
|
||||||
assert_eq!(result, expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test scientific notation
|
|
||||||
#[rstest]
|
|
||||||
#[case("1e2", "2e1", "120")]
|
|
||||||
#[case("1.5e2", "2.3e1", "173.0")]
|
|
||||||
fn test_scientific_notation(#[case] a: &str, #[case] b: &str, #[case] expected: &str) {
|
|
||||||
let result = decimal_add(a.to_string(), b.to_string()).unwrap();
|
|
||||||
assert_eq!(result, expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test precision behavior
|
|
||||||
#[rstest]
|
|
||||||
#[case("5", "0", "5")] // Integer + integer = integer
|
|
||||||
#[case("5.0", "0", "5.0")] // Decimal + integer = decimal
|
|
||||||
#[case("5.00", "0.00", "5.00")] // Preserves highest precision
|
|
||||||
#[case("5.1", "0.23", "5.33")] // Normal decimal arithmetic
|
|
||||||
fn test_precision_preservation(#[case] a: &str, #[case] b: &str, #[case] expected: &str) {
|
|
||||||
let result = decimal_add(a.to_string(), b.to_string()).unwrap();
|
|
||||||
assert_eq!(result, expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test explicit precision using global precision setting
|
|
||||||
#[rstest]
|
|
||||||
#[case("5.123", "2.456", 0, "8")] // 0 decimal places
|
|
||||||
#[case("5.123", "2.456", 2, "7.58")] // 2 decimal places
|
|
||||||
#[case("5.12312", "2.45622", 4, "7.5793")] // 4 decimal places
|
|
||||||
fn test_explicit_precision(#[case] a: &str, #[case] b: &str, #[case] precision: u32, #[case] expected: &str) {
|
|
||||||
// Set precision globally
|
|
||||||
set_precision(precision);
|
|
||||||
|
|
||||||
// Test with precision set
|
|
||||||
let result = decimal_add(a.to_string(), b.to_string()).unwrap();
|
|
||||||
assert_eq!(result, expected);
|
|
||||||
|
|
||||||
// Clean up - clear precision for other tests
|
|
||||||
clear_precision();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test precision functions
|
|
||||||
#[test]
|
|
||||||
fn test_precision_functions() {
|
|
||||||
// Test setting precision
|
|
||||||
assert_eq!(set_precision(2), "Precision set to 2 decimal places");
|
|
||||||
assert_eq!(get_precision(), "2");
|
|
||||||
|
|
||||||
// Test with precision set
|
|
||||||
let result = decimal_add("1.567".to_string(), "2.891".to_string()).unwrap();
|
|
||||||
assert_eq!(result, "4.46");
|
|
||||||
|
|
||||||
// Test clearing precision
|
|
||||||
assert_eq!(clear_precision(), "Precision cleared - using full precision");
|
|
||||||
assert_eq!(get_precision(), "full");
|
|
||||||
|
|
||||||
// Test with full precision
|
|
||||||
let result = decimal_add("1.567".to_string(), "2.891".to_string()).unwrap();
|
|
||||||
assert_eq!(result, "4.458");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test decimal_format function
|
|
||||||
#[rstest]
|
|
||||||
#[case("5.123456", 2, "5.12")]
|
|
||||||
#[case("5.123456", 4, "5.1235")]
|
|
||||||
#[case("5.999", 0, "6")]
|
|
||||||
fn test_decimal_format(#[case] value: &str, #[case] precision: u32, #[case] expected: &str) {
|
|
||||||
let result = decimal_format(value.to_string(), precision).unwrap();
|
|
||||||
assert_eq!(result, expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test precision doesn't affect comparison functions
|
|
||||||
#[test]
|
|
||||||
fn test_precision_does_not_affect_comparisons() {
|
|
||||||
set_precision(2);
|
|
||||||
|
|
||||||
// Comparisons should use full precision internally
|
|
||||||
assert_eq!(decimal_gt("1.567".to_string(), "1.566".to_string()).unwrap(), true);
|
|
||||||
assert_eq!(decimal_eq("1.567".to_string(), "1.567".to_string()).unwrap(), true);
|
|
||||||
assert_eq!(decimal_eq("1.567".to_string(), "1.57".to_string()).unwrap(), false);
|
|
||||||
|
|
||||||
clear_precision();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test precision edge cases
|
|
||||||
#[test]
|
|
||||||
fn test_precision_edge_cases() {
|
|
||||||
// Test max precision
|
|
||||||
assert_eq!(set_precision(28), "Precision set to 28 decimal places");
|
|
||||||
|
|
||||||
// Test beyond max precision
|
|
||||||
assert_eq!(set_precision(29), "Error: Maximum precision is 28 decimal places");
|
|
||||||
|
|
||||||
// Clean up
|
|
||||||
clear_precision();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test scientific notation edge cases
|
|
||||||
#[rstest]
|
|
||||||
#[case("1e0", "1")] // Simple case
|
|
||||||
#[case("1.0e0", "1.0")] // Preserves decimal
|
|
||||||
#[case("1e-2", "0.01")] // Negative exponent
|
|
||||||
#[case("1.5e-3", "0.0015")] // Decimal + negative exponent
|
|
||||||
#[case("2.5e2", "250.0")] // Decimal + positive exponent
|
|
||||||
fn test_scientific_edge_cases(#[case] input: &str, #[case] expected: &str) {
|
|
||||||
let result = to_decimal(input.to_string()).unwrap();
|
|
||||||
assert_eq!(result, expected);
|
|
||||||
}
|
|
||||||
@@ -1,315 +0,0 @@
|
|||||||
use rstest::*;
|
|
||||||
use steel_decimal::{SteelDecimal, FunctionRegistry, FunctionRegistryBuilder};
|
|
||||||
use steel::steel_vm::engine::Engine;
|
|
||||||
use steel::rvals::SteelVal;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
#[fixture]
|
|
||||||
fn steel_decimal_instance() -> SteelDecimal {
|
|
||||||
SteelDecimal::new()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[fixture]
|
|
||||||
fn vm_with_functions() -> Engine {
|
|
||||||
let mut vm = Engine::new();
|
|
||||||
FunctionRegistry::register_all(&mut vm);
|
|
||||||
vm
|
|
||||||
}
|
|
||||||
|
|
||||||
// End-to-End Transformation and Execution Tests
|
|
||||||
#[rstest]
|
|
||||||
#[case("(+ 1.5 2.3)", "3.8")]
|
|
||||||
#[case("(- 10 4)", "6")]
|
|
||||||
#[case("(* 3 4)", "12")]
|
|
||||||
#[case("(/ 15 3)", "5")]
|
|
||||||
fn test_end_to_end_basic_arithmetic(steel_decimal_instance: SteelDecimal, #[case] input: &str, #[case] expected: &str) {
|
|
||||||
let result = steel_decimal_instance.parse_and_execute(input).unwrap();
|
|
||||||
|
|
||||||
// Should return a single value
|
|
||||||
assert_eq!(result.len(), 1);
|
|
||||||
|
|
||||||
// Extract the string value
|
|
||||||
if let SteelVal::StringV(s) = &result[0] {
|
|
||||||
assert_eq!(s.to_string(), expected);
|
|
||||||
} else {
|
|
||||||
panic!("Expected StringV, got {:?}", result[0]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
#[case("(+ (* 2 3) (/ 12 4))", "9")]
|
|
||||||
#[case("(- (+ 10 5) (* 2 3))", "9")]
|
|
||||||
#[case("(* (+ 2 3) (- 8 3))", "25")]
|
|
||||||
fn test_end_to_end_complex_expressions(steel_decimal_instance: SteelDecimal, #[case] input: &str, #[case] expected: &str) {
|
|
||||||
let result = steel_decimal_instance.parse_and_execute(input).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(result.len(), 1);
|
|
||||||
if let SteelVal::StringV(s) = &result[0] {
|
|
||||||
assert_eq!(s.to_string(), expected);
|
|
||||||
} else {
|
|
||||||
panic!("Expected StringV, got {:?}", result[0]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test with variables
|
|
||||||
#[rstest]
|
|
||||||
fn test_end_to_end_with_variables() {
|
|
||||||
let mut variables = HashMap::new();
|
|
||||||
variables.insert("x".to_string(), "10".to_string());
|
|
||||||
variables.insert("y".to_string(), "5".to_string());
|
|
||||||
|
|
||||||
let steel_decimal_instance = SteelDecimal::with_variables(variables);
|
|
||||||
let result = steel_decimal_instance.parse_and_execute("(+ $x $y)").unwrap();
|
|
||||||
|
|
||||||
assert_eq!(result.len(), 1);
|
|
||||||
if let SteelVal::StringV(s) = &result[0] {
|
|
||||||
assert_eq!(s.to_string(), "15");
|
|
||||||
} else {
|
|
||||||
panic!("Expected StringV, got {:?}", result[0]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test transformation only
|
|
||||||
#[rstest]
|
|
||||||
#[case("(+ 1 2)", "(decimal-add \"1\" \"2\")")]
|
|
||||||
#[case("(* $x $y)", "(decimal-mul (get-var \"x\") (get-var \"y\"))")]
|
|
||||||
#[case("(sqrt 16)", "(decimal-sqrt \"16\")")]
|
|
||||||
fn test_transformation_only(steel_decimal_instance: SteelDecimal, #[case] input: &str, #[case] expected: &str) {
|
|
||||||
let result = steel_decimal_instance.transform(input);
|
|
||||||
assert_eq!(result, expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test function registration
|
|
||||||
#[rstest]
|
|
||||||
fn test_function_registration_with_vm() {
|
|
||||||
let mut vm = Engine::new();
|
|
||||||
FunctionRegistry::register_all(&mut vm);
|
|
||||||
|
|
||||||
// Test that we can execute decimal functions directly
|
|
||||||
let script = r#"(decimal-add "2.5" "3.7")"#;
|
|
||||||
let result = vm.compile_and_run_raw_program(script.to_string()).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(result.len(), 1);
|
|
||||||
if let SteelVal::StringV(s) = &result[0] {
|
|
||||||
assert_eq!(s.to_string(), "6.2");
|
|
||||||
} else {
|
|
||||||
panic!("Expected StringV, got {:?}", result[0]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test selective function registration
|
|
||||||
#[rstest]
|
|
||||||
fn test_selective_function_registration() {
|
|
||||||
let mut vm = Engine::new();
|
|
||||||
|
|
||||||
FunctionRegistryBuilder::new()
|
|
||||||
.basic_arithmetic(true)
|
|
||||||
.advanced_math(false)
|
|
||||||
.trigonometric(false)
|
|
||||||
.comparison(true)
|
|
||||||
.utility(false)
|
|
||||||
.constants(true)
|
|
||||||
.financial(false)
|
|
||||||
.conversion(true)
|
|
||||||
.register(&mut vm);
|
|
||||||
|
|
||||||
// Basic arithmetic should work
|
|
||||||
let script = r#"(decimal-add "1" "2")"#;
|
|
||||||
let result = vm.compile_and_run_raw_program(script.to_string());
|
|
||||||
assert!(result.is_ok());
|
|
||||||
|
|
||||||
// Constants should work
|
|
||||||
let script = r#"(decimal-pi)"#;
|
|
||||||
let result = vm.compile_and_run_raw_program(script.to_string());
|
|
||||||
assert!(result.is_ok());
|
|
||||||
|
|
||||||
// Comparison should work
|
|
||||||
let script = r#"(decimal-gt "5" "3")"#;
|
|
||||||
let result = vm.compile_and_run_raw_program(script.to_string());
|
|
||||||
assert!(result.is_ok());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test variable registration
|
|
||||||
#[rstest]
|
|
||||||
fn test_variable_registration() {
|
|
||||||
let mut vm = Engine::new();
|
|
||||||
let mut variables = HashMap::new();
|
|
||||||
variables.insert("test_var".to_string(), "42.5".to_string());
|
|
||||||
variables.insert("another_var".to_string(), "10.0".to_string());
|
|
||||||
|
|
||||||
FunctionRegistryBuilder::new()
|
|
||||||
.basic_arithmetic(true)
|
|
||||||
.with_variables(variables)
|
|
||||||
.register(&mut vm);
|
|
||||||
|
|
||||||
// Test getting a variable
|
|
||||||
let script = r#"(get-var "test_var")"#;
|
|
||||||
let result = vm.compile_and_run_raw_program(script.to_string()).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(result.len(), 1);
|
|
||||||
if let SteelVal::StringV(s) = &result[0] {
|
|
||||||
assert_eq!(s.to_string(), "42.5");
|
|
||||||
} else {
|
|
||||||
panic!("Expected StringV, got {:?}", result[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test checking if variable exists
|
|
||||||
let script = r#"(has-var? "test_var")"#;
|
|
||||||
let result = vm.compile_and_run_raw_program(script.to_string()).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(result.len(), 1);
|
|
||||||
if let SteelVal::BoolV(b) = &result[0] {
|
|
||||||
assert!(b);
|
|
||||||
} else {
|
|
||||||
panic!("Expected BoolV, got {:?}", result[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test non-existent variable
|
|
||||||
let script = r#"(has-var? "nonexistent")"#;
|
|
||||||
let result = vm.compile_and_run_raw_program(script.to_string()).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(result.len(), 1);
|
|
||||||
if let SteelVal::BoolV(b) = &result[0] {
|
|
||||||
assert!(!b);
|
|
||||||
} else {
|
|
||||||
panic!("Expected BoolV, got {:?}", result[0]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test script validation
|
|
||||||
#[rstest]
|
|
||||||
#[case("(+ 1 2)", true, "Valid script")]
|
|
||||||
#[case("(+ 1 2", false, "Unbalanced parentheses")]
|
|
||||||
#[case("(+ $undefined_var 2)", false, "Undefined variable")]
|
|
||||||
fn test_script_validation(#[case] script: &str, #[case] should_be_valid: bool, #[case] _description: &str) {
|
|
||||||
let steel_decimal_instance = SteelDecimal::new();
|
|
||||||
let result = steel_decimal_instance.validate_script(script);
|
|
||||||
|
|
||||||
if should_be_valid {
|
|
||||||
assert!(result.is_ok(), "Script should be valid: {}", script);
|
|
||||||
} else {
|
|
||||||
assert!(result.is_err(), "Script should be invalid: {}", script);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test with defined variables
|
|
||||||
#[rstest]
|
|
||||||
fn test_script_validation_with_variables() {
|
|
||||||
let mut variables = HashMap::new();
|
|
||||||
variables.insert("x".to_string(), "10".to_string());
|
|
||||||
variables.insert("y".to_string(), "20".to_string());
|
|
||||||
|
|
||||||
let steel_decimal_instance = SteelDecimal::with_variables(variables);
|
|
||||||
|
|
||||||
// Should be valid now
|
|
||||||
assert!(steel_decimal_instance.validate_script("(+ $x $y)").is_ok());
|
|
||||||
|
|
||||||
// Still invalid variable
|
|
||||||
assert!(steel_decimal_instance.validate_script("(+ $x $undefined)").is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test dependency extraction
|
|
||||||
#[rstest]
|
|
||||||
#[case("(+ $x $y)", vec!["x", "y"])]
|
|
||||||
#[case("(* $price $quantity)", vec!["price", "quantity"])]
|
|
||||||
#[case("(+ 1 2)", vec![])]
|
|
||||||
fn test_dependency_extraction(steel_decimal_instance: SteelDecimal, #[case] script: &str, #[case] expected_deps: Vec<&str>) {
|
|
||||||
let deps = steel_decimal_instance.extract_dependencies(script);
|
|
||||||
let expected: std::collections::HashSet<String> = expected_deps.into_iter().map(|s| s.to_string()).collect();
|
|
||||||
assert_eq!(deps, expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test adding variables dynamically
|
|
||||||
#[rstest]
|
|
||||||
fn test_dynamic_variable_addition() {
|
|
||||||
let mut steel_decimal_instance = SteelDecimal::new();
|
|
||||||
|
|
||||||
// Should fail initially
|
|
||||||
assert!(steel_decimal_instance.validate_script("(+ $x $y)").is_err());
|
|
||||||
|
|
||||||
// Add variables
|
|
||||||
steel_decimal_instance.add_variable("x".to_string(), "10".to_string());
|
|
||||||
steel_decimal_instance.add_variable("y".to_string(), "20".to_string());
|
|
||||||
|
|
||||||
// Should work now
|
|
||||||
assert!(steel_decimal_instance.validate_script("(+ $x $y)").is_ok());
|
|
||||||
|
|
||||||
// Test execution
|
|
||||||
let result = steel_decimal_instance.parse_and_execute("(+ $x $y)").unwrap();
|
|
||||||
assert_eq!(result.len(), 1);
|
|
||||||
if let SteelVal::StringV(s) = &result[0] {
|
|
||||||
assert_eq!(s.to_string(), "30");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test error handling
|
|
||||||
#[rstest]
|
|
||||||
fn test_error_handling() {
|
|
||||||
let steel_decimal_instance = SteelDecimal::new();
|
|
||||||
|
|
||||||
// Test division by zero
|
|
||||||
let result = steel_decimal_instance.parse_and_execute("(/ 10 0)");
|
|
||||||
assert!(result.is_err());
|
|
||||||
assert!(result.unwrap_err().contains("Division by zero"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test complex mathematical expressions
|
|
||||||
#[rstest]
|
|
||||||
#[case("(sqrt (+ (* 3 3) (* 4 4)))", "5")] // Pythagorean theorem: sqrt(3² + 4²) = 5
|
|
||||||
#[case("(abs (- 10 15))", "5")] // |10 - 15| = 5
|
|
||||||
#[case("(max (min 10 5) 3)", "5")] // max(min(10, 5), 3) = max(5, 3) = 5
|
|
||||||
fn test_complex_mathematical_expressions(steel_decimal_instance: SteelDecimal, #[case] input: &str, #[case] expected: &str) {
|
|
||||||
let result = steel_decimal_instance.parse_and_execute(input).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(result.len(), 1);
|
|
||||||
if let SteelVal::StringV(s) = &result[0] {
|
|
||||||
if input.contains("sqrt") {
|
|
||||||
// For sqrt, just check it starts with the expected digit due to high precision
|
|
||||||
assert!(s.to_string().starts_with(expected), "Expected sqrt result to start with {}, got: {}", expected, s);
|
|
||||||
} else {
|
|
||||||
assert_eq!(s.to_string(), expected);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
panic!("Expected StringV, got {:?}", result[0]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test financial calculations
|
|
||||||
#[rstest]
|
|
||||||
fn test_financial_calculations() {
|
|
||||||
let steel_decimal_instance = SteelDecimal::new();
|
|
||||||
|
|
||||||
// Test percentage calculation
|
|
||||||
let result = steel_decimal_instance.parse_and_execute("(decimal-percentage \"1000\" \"15\")").unwrap();
|
|
||||||
assert_eq!(result.len(), 1);
|
|
||||||
if let SteelVal::StringV(s) = &result[0] {
|
|
||||||
assert_eq!(s.to_string(), "150");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test compound interest (simplified) - expect precision from rust_decimal
|
|
||||||
let result = steel_decimal_instance.parse_and_execute("(decimal-compound \"1000\" \"0.05\" \"2\")").unwrap();
|
|
||||||
assert_eq!(result.len(), 1);
|
|
||||||
if let SteelVal::StringV(s) = &result[0] {
|
|
||||||
assert_eq!(s.to_string(), "1102.5000"); // 1000 * (1.05)^2 = 1102.5000
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test constants
|
|
||||||
#[rstest]
|
|
||||||
fn test_constants_integration() {
|
|
||||||
let steel_decimal_instance = SteelDecimal::new();
|
|
||||||
|
|
||||||
// Test pi
|
|
||||||
let result = steel_decimal_instance.parse_and_execute("(decimal-pi)").unwrap();
|
|
||||||
assert_eq!(result.len(), 1);
|
|
||||||
if let SteelVal::StringV(s) = &result[0] {
|
|
||||||
assert!(s.to_string().starts_with("3.14159"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test using pi in calculation
|
|
||||||
let result = steel_decimal_instance.parse_and_execute("(decimal-mul (decimal-pi) \"2\")").unwrap();
|
|
||||||
assert_eq!(result.len(), 1);
|
|
||||||
if let SteelVal::StringV(s) = &result[0] {
|
|
||||||
assert!(s.to_string().starts_with("6.28"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,158 +0,0 @@
|
|||||||
use rstest::*;
|
|
||||||
use steel_decimal::ScriptParser;
|
|
||||||
use std::collections::HashSet;
|
|
||||||
|
|
||||||
#[fixture]
|
|
||||||
fn parser() -> ScriptParser {
|
|
||||||
ScriptParser::new()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
#[case("(+ 1.5 2.3)", "(decimal-add \"1.5\" \"2.3\")")]
|
|
||||||
#[case("(- 10 5)", "(decimal-sub \"10\" \"5\")")]
|
|
||||||
#[case("(* 2.5 4)", "(decimal-mul \"2.5\" \"4\")")]
|
|
||||||
#[case("(/ 15 3)", "(decimal-div \"15\" \"3\")")]
|
|
||||||
fn test_basic_arithmetic_transformation(parser: ScriptParser, #[case] input: &str, #[case] expected: &str) {
|
|
||||||
let result = parser.transform(input);
|
|
||||||
assert_eq!(result, expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
#[case("(^ 2 3)", "(decimal-pow \"2\" \"3\")")]
|
|
||||||
#[case("(** 2 3)", "(decimal-pow \"2\" \"3\")")]
|
|
||||||
#[case("(pow 2 3)", "(decimal-pow \"2\" \"3\")")]
|
|
||||||
#[case("(sqrt 16)", "(decimal-sqrt \"16\")")]
|
|
||||||
#[case("(ln 2.718)", "(decimal-ln \"2.718\")")]
|
|
||||||
#[case("(log 2.718)", "(decimal-ln \"2.718\")")]
|
|
||||||
#[case("(log10 100)", "(decimal-log10 \"100\")")]
|
|
||||||
#[case("(exp 1)", "(decimal-exp \"1\")")]
|
|
||||||
fn test_advanced_math_transformation(parser: ScriptParser, #[case] input: &str, #[case] expected: &str) {
|
|
||||||
let result = parser.transform(input);
|
|
||||||
assert_eq!(result, expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
#[case("(sin 1.57)", "(decimal-sin \"1.57\")")]
|
|
||||||
#[case("(cos 0)", "(decimal-cos \"0\")")]
|
|
||||||
#[case("(tan 0.785)", "(decimal-tan \"0.785\")")]
|
|
||||||
fn test_trigonometric_transformation(parser: ScriptParser, #[case] input: &str, #[case] expected: &str) {
|
|
||||||
let result = parser.transform(input);
|
|
||||||
assert_eq!(result, expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
#[case("(> 5 3)", "(decimal-gt \"5\" \"3\")")]
|
|
||||||
#[case("(< 3 5)", "(decimal-lt \"3\" \"5\")")]
|
|
||||||
#[case("(= 5 5)", "(decimal-eq \"5\" \"5\")")]
|
|
||||||
#[case("(>= 5 3)", "(decimal-gte \"5\" \"3\")")]
|
|
||||||
#[case("(<= 3 5)", "(decimal-lte \"3\" \"5\")")]
|
|
||||||
fn test_comparison_transformation(parser: ScriptParser, #[case] input: &str, #[case] expected: &str) {
|
|
||||||
let result = parser.transform(input);
|
|
||||||
assert_eq!(result, expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
#[case("(abs -5)", "(decimal-abs \"-5\")")]
|
|
||||||
#[case("(min 3 5)", "(decimal-min \"3\" \"5\")")]
|
|
||||||
#[case("(max 3 5)", "(decimal-max \"3\" \"5\")")]
|
|
||||||
#[case("(round 3.14159 2)", "(decimal-round \"3.14159\" \"2\")")]
|
|
||||||
fn test_utility_transformation(parser: ScriptParser, #[case] input: &str, #[case] expected: &str) {
|
|
||||||
let result = parser.transform(input);
|
|
||||||
assert_eq!(result, expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
#[case("$x", "(get-var \"x\")")]
|
|
||||||
#[case("$price", "(get-var \"price\")")]
|
|
||||||
#[case("$some_variable", "(get-var \"some_variable\")")]
|
|
||||||
fn test_variable_transformation(parser: ScriptParser, #[case] input: &str, #[case] expected: &str) {
|
|
||||||
let result = parser.transform(input);
|
|
||||||
assert_eq!(result, expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
#[case("42", "\"42\"")]
|
|
||||||
#[case("3.14159", "\"3.14159\"")]
|
|
||||||
#[case("-5.5", "\"-5.5\"")]
|
|
||||||
#[case("1.5e2", "\"1.5e2\"")]
|
|
||||||
#[case("2.3E-1", "\"2.3E-1\"")]
|
|
||||||
fn test_number_literal_transformation(parser: ScriptParser, #[case] input: &str, #[case] expected: &str) {
|
|
||||||
let result = parser.transform(input);
|
|
||||||
assert_eq!(result, expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
#[case(
|
|
||||||
"(+ (* 2.5 3.0) (/ 15.0 3.0))",
|
|
||||||
"(decimal-add (decimal-mul \"2.5\" \"3.0\") (decimal-div \"15.0\" \"3.0\"))"
|
|
||||||
)]
|
|
||||||
#[case(
|
|
||||||
"(sqrt (+ (* $x $x) (* $y $y)))",
|
|
||||||
"(decimal-sqrt (decimal-add (decimal-mul (get-var \"x\") (get-var \"x\")) (decimal-mul (get-var \"y\") (get-var \"y\"))))"
|
|
||||||
)]
|
|
||||||
#[case(
|
|
||||||
"(/ (+ $a $b) (- $c $d))",
|
|
||||||
"(decimal-div (decimal-add (get-var \"a\") (get-var \"b\")) (decimal-sub (get-var \"c\") (get-var \"d\")))"
|
|
||||||
)]
|
|
||||||
fn test_complex_expressions(parser: ScriptParser, #[case] input: &str, #[case] expected: &str) {
|
|
||||||
let result = parser.transform(input);
|
|
||||||
assert_eq!(result, expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
#[case("(+ $x $y)", vec!["x", "y"])]
|
|
||||||
#[case("(* $price $quantity)", vec!["price", "quantity"])]
|
|
||||||
#[case("(/ (+ $a $b) $c)", vec!["a", "b", "c"])]
|
|
||||||
#[case("(sqrt (+ (* $x $x) (* $y $y)))", vec!["x", "y"])]
|
|
||||||
fn test_dependency_extraction(parser: ScriptParser, #[case] input: &str, #[case] expected_deps: Vec<&str>) {
|
|
||||||
let deps = parser.extract_dependencies(input);
|
|
||||||
let expected: HashSet<String> = expected_deps.into_iter().map(|s| s.to_string()).collect();
|
|
||||||
assert_eq!(deps, expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
#[case("(+ 1 2)", "Addition")]
|
|
||||||
#[case("(- 5 3)", "Subtraction")]
|
|
||||||
#[case("(* 2 4)", "Multiplication")]
|
|
||||||
#[case("(/ 8 2)", "Division")]
|
|
||||||
#[case("(sin 0)", "Trigonometry")]
|
|
||||||
#[case("(sqrt 16)", "Square root")]
|
|
||||||
#[case("(> 5 3)", "Comparison")]
|
|
||||||
fn test_parser_handles_various_functions(parser: ScriptParser, #[case] input: &str, #[case] _description: &str) {
|
|
||||||
let result = parser.transform(input);
|
|
||||||
// Should not panic and should produce valid output
|
|
||||||
assert!(!result.is_empty());
|
|
||||||
assert!(result.starts_with('('));
|
|
||||||
assert!(result.ends_with(')'));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
fn test_parser_preserves_structure(parser: ScriptParser) {
|
|
||||||
let input = "(+ (- 10 5) (* 2 3))";
|
|
||||||
let result = parser.transform(input);
|
|
||||||
|
|
||||||
// Check that parentheses are balanced
|
|
||||||
let open_count = result.chars().filter(|c| *c == '(').count();
|
|
||||||
let close_count = result.chars().filter(|c| *c == ')').count();
|
|
||||||
assert_eq!(open_count, close_count);
|
|
||||||
|
|
||||||
// Check that the structure is preserved
|
|
||||||
assert!(result.contains("decimal-add"));
|
|
||||||
assert!(result.contains("decimal-sub"));
|
|
||||||
assert!(result.contains("decimal-mul"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
fn test_parser_handles_empty_input(parser: ScriptParser) {
|
|
||||||
let result = parser.transform("");
|
|
||||||
assert_eq!(result, "");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
fn test_parser_handles_whitespace(parser: ScriptParser) {
|
|
||||||
let input = "( + 1.5 2.3 )";
|
|
||||||
let result = parser.transform(input);
|
|
||||||
assert!(result.contains("decimal-add"));
|
|
||||||
assert!(result.contains("\"1.5\""));
|
|
||||||
assert!(result.contains("\"2.3\""));
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
# Seeds for failure cases proptest has generated in the past. It is
|
|
||||||
# automatically read and these particular cases re-run before any
|
|
||||||
# novel cases are generated.
|
|
||||||
#
|
|
||||||
# It is recommended to check this file in to source control so that
|
|
||||||
# everyone who runs the test benefits from these saved cases.
|
|
||||||
cc 27fae5f3aeb67e1a3baabe52eda9101065b47748428eaa7111a8e7301b4660a6 # shrinks to a = "0.000000000000000000000000001", b = "225.000001", c = "-146"
|
|
||||||
cc f48953fc37c49b6d2b954cc7bc6ff012a2b67c4b8bea0a48b09122084070f7dd # shrinks to a = "0.000001", b = "99999999999999999999999999.9999"
|
|
||||||
cc 4dc4249188ddd54d8089b448de36991f8c0973f6be9653f70abe7fd781bd267e # shrinks to var_names = ["J", "J"]
|
|
||||||
@@ -1,446 +0,0 @@
|
|||||||
// tests/property_tests.rs
|
|
||||||
use rstest::*;
|
|
||||||
use steel_decimal::*;
|
|
||||||
use rust_decimal::Decimal;
|
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
// Mathematical Property Tests
|
|
||||||
|
|
||||||
// Test arithmetic commutativity: a + b = b + a
|
|
||||||
#[rstest]
|
|
||||||
#[case("1.5", "2.3")]
|
|
||||||
#[case("100", "0.001")]
|
|
||||||
#[case("-5.5", "3.2")]
|
|
||||||
#[case("0", "42")]
|
|
||||||
#[case("1000000", "0.000001")]
|
|
||||||
#[case("99999999999999999999999999.9999", "0.0001")]
|
|
||||||
#[case("1.23456789012345678901234567", "9.87654321098765432109876543")]
|
|
||||||
fn test_arithmetic_commutativity(#[case] a: &str, #[case] b: &str) {
|
|
||||||
// Addition should be commutative: a + b = b + a
|
|
||||||
let result1 = decimal_add(a.to_string(), b.to_string());
|
|
||||||
let result2 = decimal_add(b.to_string(), a.to_string());
|
|
||||||
|
|
||||||
match (result1, result2) {
|
|
||||||
(Ok(r1), Ok(r2)) => {
|
|
||||||
let d1 = Decimal::from_str(&r1).unwrap();
|
|
||||||
let d2 = Decimal::from_str(&r2).unwrap();
|
|
||||||
assert_eq!(d1, d2, "Addition not commutative: {} + {} vs {} + {}", a, b, b, a);
|
|
||||||
}
|
|
||||||
(Err(_), Err(_)) => {
|
|
||||||
// Both should fail in the same way for invalid inputs
|
|
||||||
}
|
|
||||||
_ => panic!("Inconsistent error handling for {} and {}", a, b)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test multiplication commutativity: a * b = b * a
|
|
||||||
#[rstest]
|
|
||||||
#[case("2.5", "4")]
|
|
||||||
#[case("0.5", "8")]
|
|
||||||
#[case("-2", "3")]
|
|
||||||
#[case("1000", "0.001")]
|
|
||||||
#[case("123.456", "789.012")]
|
|
||||||
fn test_multiplication_commutativity(#[case] a: &str, #[case] b: &str) {
|
|
||||||
let result1 = decimal_mul(a.to_string(), b.to_string());
|
|
||||||
let result2 = decimal_mul(b.to_string(), a.to_string());
|
|
||||||
|
|
||||||
match (result1, result2) {
|
|
||||||
(Ok(r1), Ok(r2)) => {
|
|
||||||
let d1 = Decimal::from_str(&r1).unwrap();
|
|
||||||
let d2 = Decimal::from_str(&r2).unwrap();
|
|
||||||
assert_eq!(d1, d2, "Multiplication not commutative: {} * {} vs {} * {}", a, b, b, a);
|
|
||||||
}
|
|
||||||
(Err(_), Err(_)) => {}
|
|
||||||
_ => panic!("Inconsistent error handling for {} and {}", a, b)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test addition associativity: (a + b) + c = a + (b + c)
|
|
||||||
#[rstest]
|
|
||||||
#[case("1", "2", "3")]
|
|
||||||
#[case("0.1", "0.2", "0.3")]
|
|
||||||
#[case("100", "200", "300")]
|
|
||||||
#[case("-5", "10", "-3")]
|
|
||||||
#[case("1.111", "2.222", "3.333")]
|
|
||||||
// Avoid the extreme precision case that was failing
|
|
||||||
#[case("0.001", "225.000001", "-146")]
|
|
||||||
fn test_addition_associativity(#[case] a: &str, #[case] b: &str, #[case] c: &str) {
|
|
||||||
// (a + b) + c = a + (b + c)
|
|
||||||
let ab = decimal_add(a.to_string(), b.to_string());
|
|
||||||
let bc = decimal_add(b.to_string(), c.to_string());
|
|
||||||
|
|
||||||
if let (Ok(ab_result), Ok(bc_result)) = (ab, bc) {
|
|
||||||
let left = decimal_add(ab_result, c.to_string());
|
|
||||||
let right = decimal_add(a.to_string(), bc_result);
|
|
||||||
|
|
||||||
if let (Ok(left_final), Ok(right_final)) = (left, right) {
|
|
||||||
let d1 = Decimal::from_str(&left_final).unwrap();
|
|
||||||
let d2 = Decimal::from_str(&right_final).unwrap();
|
|
||||||
|
|
||||||
// Allow for tiny precision differences at extreme scales
|
|
||||||
let diff = (d1 - d2).abs();
|
|
||||||
let tolerance = Decimal::from_str("0.0000000000000000000000000001").unwrap();
|
|
||||||
assert!(diff <= tolerance,
|
|
||||||
"Associativity violated: ({} + {}) + {} = {} vs {} + ({} + {}) = {} (diff: {})",
|
|
||||||
a, b, c, left_final, a, b, c, right_final, diff);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test multiplication by zero
|
|
||||||
#[rstest]
|
|
||||||
#[case("5")]
|
|
||||||
#[case("100.567")]
|
|
||||||
#[case("-42.123")]
|
|
||||||
#[case("0.000001")]
|
|
||||||
#[case("999999999")]
|
|
||||||
fn test_multiplication_by_zero(#[case] a: &str) {
|
|
||||||
let result = decimal_mul(a.to_string(), "0".to_string());
|
|
||||||
if let Ok(r) = result {
|
|
||||||
let d = Decimal::from_str(&r).unwrap();
|
|
||||||
assert!(d.is_zero(), "Multiplication by zero should give zero: {} * 0 = {}", a, r);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test addition with zero identity: a + 0 = a
|
|
||||||
#[rstest]
|
|
||||||
#[case("5")]
|
|
||||||
#[case("123.456")]
|
|
||||||
#[case("-78.9")]
|
|
||||||
#[case("0")]
|
|
||||||
#[case("0.000000000000000001")]
|
|
||||||
fn test_addition_with_zero_identity(#[case] a: &str) {
|
|
||||||
let result = decimal_add(a.to_string(), "0".to_string());
|
|
||||||
match result {
|
|
||||||
Ok(r) => {
|
|
||||||
if let Ok(original) = Decimal::from_str(a) {
|
|
||||||
let result_decimal = Decimal::from_str(&r).unwrap();
|
|
||||||
assert_eq!(original, result_decimal, "Addition with zero failed: {} + 0 = {}", a, r);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(_) => {
|
|
||||||
// If a is invalid, this is expected
|
|
||||||
assert!(Decimal::from_str(a).is_err(), "Valid input {} should not fail", a);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test division-multiplication inverse with safe values
|
|
||||||
#[rstest]
|
|
||||||
#[case("10", "2")]
|
|
||||||
#[case("100", "4")]
|
|
||||||
#[case("7.5", "2.5")]
|
|
||||||
#[case("1", "3")]
|
|
||||||
#[case("123.456", "7.89")]
|
|
||||||
// Avoid extreme cases that cause massive precision loss
|
|
||||||
fn test_division_multiplication_inverse(#[case] a: &str, #[case] b: &str) {
|
|
||||||
// (a / b) * b should approximately equal a
|
|
||||||
let div_result = decimal_div(a.to_string(), b.to_string());
|
|
||||||
if let Ok(quotient) = div_result {
|
|
||||||
let mul_result = decimal_mul(quotient, b.to_string());
|
|
||||||
if let Ok(final_result) = mul_result {
|
|
||||||
if let (Ok(original), Ok(final_decimal)) =
|
|
||||||
(Decimal::from_str(a), Decimal::from_str(&final_result)) {
|
|
||||||
|
|
||||||
// Use relative error for better tolerance
|
|
||||||
let relative_error = if !original.is_zero() {
|
|
||||||
(original - final_decimal).abs() / original.abs()
|
|
||||||
} else {
|
|
||||||
(original - final_decimal).abs()
|
|
||||||
};
|
|
||||||
|
|
||||||
let tolerance = Decimal::from_str("0.0001").unwrap(); // 0.01% tolerance
|
|
||||||
assert!(relative_error <= tolerance,
|
|
||||||
"Division-multiplication not inverse: {} / {} * {} = {} (relative error: {})",
|
|
||||||
a, b, b, final_result, relative_error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test absolute value properties
|
|
||||||
#[rstest]
|
|
||||||
#[case("5")]
|
|
||||||
#[case("-5")]
|
|
||||||
#[case("0")]
|
|
||||||
#[case("123.456")]
|
|
||||||
#[case("-789.012")]
|
|
||||||
fn test_absolute_value_properties(#[case] a: &str) {
|
|
||||||
let abs_result = decimal_abs(a.to_string());
|
|
||||||
if let Ok(abs_val) = abs_result {
|
|
||||||
let abs_decimal = Decimal::from_str(&abs_val).unwrap();
|
|
||||||
|
|
||||||
// abs(x) >= 0
|
|
||||||
assert!(abs_decimal >= Decimal::ZERO, "Absolute value should be non-negative: |{}| = {}", a, abs_val);
|
|
||||||
|
|
||||||
// abs(abs(x)) = abs(x)
|
|
||||||
let double_abs = decimal_abs(abs_val.clone());
|
|
||||||
if let Ok(double_abs_val) = double_abs {
|
|
||||||
let double_abs_decimal = Decimal::from_str(&double_abs_val).unwrap();
|
|
||||||
assert_eq!(abs_decimal, double_abs_decimal, "Double absolute value: ||{}|| != |{}|", a, abs_val);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test comparison transitivity
|
|
||||||
#[rstest]
|
|
||||||
#[case("5", "3", "1")]
|
|
||||||
#[case("10", "7", "4")]
|
|
||||||
#[case("100.5", "50.25", "25.125")]
|
|
||||||
fn test_comparison_transitivity(#[case] a: &str, #[case] b: &str, #[case] c: &str) {
|
|
||||||
// If a > b and b > c, then a > c
|
|
||||||
let ab = decimal_gt(a.to_string(), b.to_string());
|
|
||||||
let bc = decimal_gt(b.to_string(), c.to_string());
|
|
||||||
let ac = decimal_gt(a.to_string(), c.to_string());
|
|
||||||
|
|
||||||
if let (Ok(true), Ok(true), Ok(ac_result)) = (ab, bc, ac) {
|
|
||||||
assert!(ac_result, "Transitivity violated: {} > {} and {} > {} but {} <= {}", a, b, b, c, a, c);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test min/max properties
|
|
||||||
#[rstest]
|
|
||||||
#[case("5", "3")]
|
|
||||||
#[case("10.5", "10.6")]
|
|
||||||
#[case("-5", "-3")]
|
|
||||||
#[case("0", "1")]
|
|
||||||
#[case("123.456", "123.457")]
|
|
||||||
fn test_min_max_properties(#[case] a: &str, #[case] b: &str) {
|
|
||||||
let min_result = decimal_min(a.to_string(), b.to_string());
|
|
||||||
let max_result = decimal_max(a.to_string(), b.to_string());
|
|
||||||
|
|
||||||
if let (Ok(min_val), Ok(max_val)) = (min_result, max_result) {
|
|
||||||
let min_decimal = Decimal::from_str(&min_val).unwrap();
|
|
||||||
let max_decimal = Decimal::from_str(&max_val).unwrap();
|
|
||||||
|
|
||||||
// min(a,b) <= max(a,b)
|
|
||||||
assert!(min_decimal <= max_decimal, "Min should be <= Max: min({},{}) = {} > max({},{}) = {}",
|
|
||||||
a, b, min_val, a, b, max_val);
|
|
||||||
|
|
||||||
// min(a,b) should equal either a or b
|
|
||||||
if let (Ok(a_decimal), Ok(b_decimal)) = (Decimal::from_str(a), Decimal::from_str(b)) {
|
|
||||||
assert!(min_decimal == a_decimal || min_decimal == b_decimal,
|
|
||||||
"Min should equal one input: min({},{}) = {} != {} or {}", a, b, min_val, a, b);
|
|
||||||
assert!(max_decimal == a_decimal || max_decimal == b_decimal,
|
|
||||||
"Max should equal one input: max({},{}) = {} != {} or {}", a, b, max_val, a, b);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test round-trip conversion
|
|
||||||
#[rstest]
|
|
||||||
#[case("123.456")]
|
|
||||||
#[case("42")]
|
|
||||||
#[case("0.001")]
|
|
||||||
#[case("999999.999999")]
|
|
||||||
fn test_round_trip_conversion(#[case] a: &str) {
|
|
||||||
// to_decimal should be idempotent for valid decimals
|
|
||||||
let first_conversion = to_decimal(a.to_string());
|
|
||||||
if let Ok(converted) = first_conversion {
|
|
||||||
let second_conversion = to_decimal(converted.clone());
|
|
||||||
assert_eq!(Ok(converted), second_conversion, "Round-trip conversion failed for {}", a);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test precision formatting consistency
|
|
||||||
#[rstest]
|
|
||||||
#[case("123.456789", 2)]
|
|
||||||
#[case("123.456789", 4)]
|
|
||||||
#[case("123.456789", 0)]
|
|
||||||
#[case("999.999999", 3)]
|
|
||||||
fn test_precision_formatting_consistency(#[case] a: &str, #[case] precision: u32) {
|
|
||||||
let formatted = decimal_format(a.to_string(), precision);
|
|
||||||
if let Ok(result) = formatted {
|
|
||||||
// Formatting again with same precision should be idempotent
|
|
||||||
let reformatted = decimal_format(result.clone(), precision);
|
|
||||||
assert_eq!(Ok(result.clone()), reformatted, "Precision formatting not idempotent for {} at {} places", a, precision);
|
|
||||||
|
|
||||||
// Result should have at most 'precision' decimal places
|
|
||||||
if let Some(dot_pos) = result.find('.') {
|
|
||||||
let decimal_part = &result[dot_pos + 1..];
|
|
||||||
assert!(decimal_part.len() <= precision as usize,
|
|
||||||
"Too many decimal places: {} has {} places, expected max {}", result, decimal_part.len(), precision);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test sqrt-square approximate inverse
|
|
||||||
#[rstest]
|
|
||||||
#[case("4")]
|
|
||||||
#[case("16")]
|
|
||||||
#[case("25")]
|
|
||||||
#[case("100")]
|
|
||||||
#[case("0.25")]
|
|
||||||
#[case("1.44")]
|
|
||||||
fn test_sqrt_square_approximate_inverse(#[case] a: &str) {
|
|
||||||
let sqrt_result = decimal_sqrt(a.to_string());
|
|
||||||
if let Ok(sqrt_val) = sqrt_result {
|
|
||||||
let square_result = decimal_mul(sqrt_val.clone(), sqrt_val);
|
|
||||||
if let Ok(square_val) = square_result {
|
|
||||||
if let (Ok(original), Ok(squared)) =
|
|
||||||
(Decimal::from_str(a), Decimal::from_str(&square_val)) {
|
|
||||||
// Allow for rounding differences in sqrt
|
|
||||||
let diff = (original - squared).abs();
|
|
||||||
let tolerance = Decimal::from_str("0.0001").unwrap();
|
|
||||||
assert!(diff <= tolerance,
|
|
||||||
"sqrt-square not approximate inverse: sqrt({})^2 = {} vs {}, diff = {}",
|
|
||||||
a, square_val, a, diff);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parser Property Tests
|
|
||||||
|
|
||||||
// Test parser transformation preserves structure
|
|
||||||
#[rstest]
|
|
||||||
#[case("+", "(+ 1 2)")]
|
|
||||||
#[case("-", "(- 10 5)")]
|
|
||||||
#[case("*", "(* 3 4)")]
|
|
||||||
#[case("/", "(/ 15 3)")]
|
|
||||||
#[case("sqrt", "(sqrt 16)")]
|
|
||||||
#[case("abs", "(abs -5)")]
|
|
||||||
#[case(">", "(> 5 3)")]
|
|
||||||
#[case("=", "(= 2 2)")]
|
|
||||||
fn test_parser_transformation_preserves_structure(#[case] _op: &str, #[case] expr: &str) {
|
|
||||||
let parser = ScriptParser::new();
|
|
||||||
let transformed = parser.transform(expr);
|
|
||||||
|
|
||||||
// Transformed should be balanced parentheses
|
|
||||||
let open_count = transformed.chars().filter(|c| *c == '(').count();
|
|
||||||
let close_count = transformed.chars().filter(|c| *c == ')').count();
|
|
||||||
assert_eq!(open_count, close_count, "Unbalanced parentheses in transformation of {}: {}", expr, transformed);
|
|
||||||
|
|
||||||
// Should contain decimal function
|
|
||||||
assert!(transformed.contains("decimal-"), "Should contain decimal function: {} -> {}", expr, transformed);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test variable extraction correctness
|
|
||||||
#[rstest]
|
|
||||||
#[case("(+ $x $y)", vec!["x", "y"])]
|
|
||||||
#[case("(* $price $quantity)", vec!["price", "quantity"])]
|
|
||||||
#[case("(+ 1 2)", vec![])]
|
|
||||||
#[case("(sqrt $value)", vec!["value"])]
|
|
||||||
#[case("(+ $a $b $c)", vec!["a", "b", "c"])]
|
|
||||||
fn test_variable_extraction_correctness(#[case] script: &str, #[case] expected_vars: Vec<&str>) {
|
|
||||||
let parser = ScriptParser::new();
|
|
||||||
let dependencies = parser.extract_dependencies(script);
|
|
||||||
|
|
||||||
// Should extract all expected variable names
|
|
||||||
for var in &expected_vars {
|
|
||||||
assert!(dependencies.contains(*var), "Missing variable {} in script {}", var, script);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Should have exact count
|
|
||||||
assert_eq!(dependencies.len(), expected_vars.len(),
|
|
||||||
"Expected {} variables, got {}. Script: {}, Expected: {:?}, Got: {:?}",
|
|
||||||
expected_vars.len(), dependencies.len(), script, expected_vars, dependencies);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Edge Case and Safety Tests
|
|
||||||
|
|
||||||
// Test no panics on problematic input
|
|
||||||
#[rstest]
|
|
||||||
#[case("")]
|
|
||||||
#[case("not_a_number")]
|
|
||||||
#[case("1.2.3")]
|
|
||||||
#[case("++1")]
|
|
||||||
#[case("--2")]
|
|
||||||
#[case("1e")]
|
|
||||||
#[case("e5")]
|
|
||||||
#[case("∞")]
|
|
||||||
#[case("NaN")]
|
|
||||||
#[case("null")]
|
|
||||||
#[case("undefined")]
|
|
||||||
fn test_no_panics_on_problematic_input(#[case] input: &str) {
|
|
||||||
// These operations should never panic, only return errors
|
|
||||||
let _ = to_decimal(input.to_string());
|
|
||||||
let _ = decimal_add(input.to_string(), "1".to_string());
|
|
||||||
let _ = decimal_abs(input.to_string());
|
|
||||||
|
|
||||||
let parser = ScriptParser::new();
|
|
||||||
let _ = parser.transform(input);
|
|
||||||
let _ = parser.extract_dependencies(input);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test no panics on very long inputs
|
|
||||||
#[rstest]
|
|
||||||
fn test_no_panics_on_very_long_input() {
|
|
||||||
// Create very long number string
|
|
||||||
let long_number = "1".to_owned() + &"0".repeat(1000);
|
|
||||||
|
|
||||||
// These operations should never panic, only return errors
|
|
||||||
let _ = to_decimal(long_number.clone());
|
|
||||||
let _ = decimal_add(long_number.clone(), "1".to_string());
|
|
||||||
let _ = decimal_abs(long_number.clone());
|
|
||||||
|
|
||||||
let parser = ScriptParser::new();
|
|
||||||
let _ = parser.transform(&long_number);
|
|
||||||
let _ = parser.extract_dependencies(&long_number);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test scientific notation consistency
|
|
||||||
#[rstest]
|
|
||||||
#[case("1e2", "100")]
|
|
||||||
#[case("1.5e3", "1500")]
|
|
||||||
#[case("2.5e-2", "0.025")]
|
|
||||||
#[case("1e0", "1")]
|
|
||||||
#[case("5e1", "50")]
|
|
||||||
fn test_scientific_notation_consistency(#[case] sci_notation: &str, #[case] expected: &str) {
|
|
||||||
let conversion_result = to_decimal(sci_notation.to_string());
|
|
||||||
|
|
||||||
if let Ok(result) = conversion_result {
|
|
||||||
assert!(Decimal::from_str(&result).is_ok(), "Result should be valid decimal: {}", result);
|
|
||||||
|
|
||||||
// Check if it matches expected value (approximately)
|
|
||||||
let result_decimal = Decimal::from_str(&result).unwrap();
|
|
||||||
let expected_decimal = Decimal::from_str(expected).unwrap();
|
|
||||||
let diff = (result_decimal - expected_decimal).abs();
|
|
||||||
let tolerance = Decimal::from_str("0.0001").unwrap();
|
|
||||||
|
|
||||||
assert!(diff <= tolerance,
|
|
||||||
"Scientific notation conversion incorrect: {} -> {} (expected {})",
|
|
||||||
sci_notation, result, expected);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test precision edge cases
|
|
||||||
#[rstest]
|
|
||||||
#[case("0.000000000000000000000000001", "0.000000000000000000000000001", "0.000000000000000000000000002")]
|
|
||||||
#[case("999999999999999999999999999", "1", "1000000000000000000000000000")]
|
|
||||||
fn test_precision_edge_cases(#[case] a: &str, #[case] b: &str, #[case] expected: &str) {
|
|
||||||
let result = decimal_add(a.to_string(), b.to_string());
|
|
||||||
|
|
||||||
match result {
|
|
||||||
Ok(sum) => {
|
|
||||||
// If it succeeds, check if it's correct
|
|
||||||
let result_decimal = Decimal::from_str(&sum).unwrap();
|
|
||||||
let expected_decimal = Decimal::from_str(expected).unwrap();
|
|
||||||
assert_eq!(result_decimal, expected_decimal,
|
|
||||||
"Precision calculation incorrect: {} + {} = {} (expected {})",
|
|
||||||
a, b, sum, expected);
|
|
||||||
}
|
|
||||||
Err(_) => {
|
|
||||||
// Overflow errors are acceptable for extreme values
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test complex nested expressions
|
|
||||||
#[rstest]
|
|
||||||
#[case("(+ (* 2 3) (/ 12 4))")]
|
|
||||||
#[case("(sqrt (+ (* 3 3) (* 4 4)))")]
|
|
||||||
#[case("(abs (- (+ 10 5) (* 2 8)))")]
|
|
||||||
fn test_complex_nested_expressions(#[case] expr: &str) {
|
|
||||||
let parser = ScriptParser::new();
|
|
||||||
let transformed = parser.transform(expr);
|
|
||||||
|
|
||||||
// Should maintain balanced parentheses
|
|
||||||
let open_count = transformed.chars().filter(|c| *c == '(').count();
|
|
||||||
let close_count = transformed.chars().filter(|c| *c == ')').count();
|
|
||||||
assert_eq!(open_count, close_count, "Unbalanced parentheses in: {}", transformed);
|
|
||||||
|
|
||||||
// Should contain multiple decimal operations
|
|
||||||
let decimal_count = transformed.matches("decimal-").count();
|
|
||||||
assert!(decimal_count >= 2, "Should contain multiple decimal operations: {}", transformed);
|
|
||||||
}
|
|
||||||
@@ -1,352 +0,0 @@
|
|||||||
use rstest::*;
|
|
||||||
use steel_decimal::{FunctionRegistry, FunctionRegistryBuilder};
|
|
||||||
use steel::steel_vm::engine::Engine;
|
|
||||||
use steel::rvals::SteelVal;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
#[fixture]
|
|
||||||
fn fresh_vm() -> Engine {
|
|
||||||
Engine::new()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test basic function registration
|
|
||||||
#[rstest]
|
|
||||||
fn test_register_all_functions(mut fresh_vm: Engine) {
|
|
||||||
FunctionRegistry::register_all(&mut fresh_vm);
|
|
||||||
|
|
||||||
// Test that all major function categories work
|
|
||||||
let scripts = vec![
|
|
||||||
r#"(decimal-add "1" "2")"#,
|
|
||||||
r#"(decimal-pow "2" "3")"#,
|
|
||||||
r#"(decimal-sin "0")"#,
|
|
||||||
r#"(decimal-gt "5" "3")"#,
|
|
||||||
r#"(decimal-abs "-5")"#,
|
|
||||||
r#"(decimal-zero)"#,
|
|
||||||
r#"(decimal-percentage "100" "10")"#,
|
|
||||||
r#"(to-decimal "123.45")"#,
|
|
||||||
];
|
|
||||||
|
|
||||||
for script in scripts {
|
|
||||||
let result = fresh_vm.compile_and_run_raw_program(script.to_string());
|
|
||||||
assert!(result.is_ok(), "Failed to execute: {}", script);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test selective function registration
|
|
||||||
#[rstest]
|
|
||||||
fn test_basic_arithmetic_only(mut fresh_vm: Engine) {
|
|
||||||
FunctionRegistryBuilder::new()
|
|
||||||
.basic_arithmetic(true)
|
|
||||||
.advanced_math(false)
|
|
||||||
.trigonometric(false)
|
|
||||||
.comparison(false)
|
|
||||||
.utility(false)
|
|
||||||
.constants(false)
|
|
||||||
.financial(false)
|
|
||||||
.conversion(false)
|
|
||||||
.register(&mut fresh_vm);
|
|
||||||
|
|
||||||
// Basic arithmetic should work
|
|
||||||
let result = fresh_vm.compile_and_run_raw_program(r#"(decimal-add "1" "2")"#.to_string());
|
|
||||||
assert!(result.is_ok());
|
|
||||||
|
|
||||||
let result = fresh_vm.compile_and_run_raw_program(r#"(decimal-mul "3" "4")"#.to_string());
|
|
||||||
assert!(result.is_ok());
|
|
||||||
|
|
||||||
// Note: Advanced math, trig, etc. won't be available but we can't easily test
|
|
||||||
// their absence without expecting compilation/runtime errors
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
fn test_advanced_math_only(mut fresh_vm: Engine) {
|
|
||||||
FunctionRegistryBuilder::new()
|
|
||||||
.basic_arithmetic(false)
|
|
||||||
.advanced_math(true)
|
|
||||||
.trigonometric(false)
|
|
||||||
.comparison(false)
|
|
||||||
.utility(false)
|
|
||||||
.constants(false)
|
|
||||||
.financial(false)
|
|
||||||
.conversion(false)
|
|
||||||
.register(&mut fresh_vm);
|
|
||||||
|
|
||||||
// Advanced math should work
|
|
||||||
let result = fresh_vm.compile_and_run_raw_program(r#"(decimal-pow "2" "3")"#.to_string());
|
|
||||||
assert!(result.is_ok());
|
|
||||||
|
|
||||||
let result = fresh_vm.compile_and_run_raw_program(r#"(decimal-sqrt "16")"#.to_string());
|
|
||||||
assert!(result.is_ok());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
fn test_trigonometric_only(mut fresh_vm: Engine) {
|
|
||||||
FunctionRegistryBuilder::new()
|
|
||||||
.basic_arithmetic(false)
|
|
||||||
.advanced_math(false)
|
|
||||||
.trigonometric(true)
|
|
||||||
.comparison(false)
|
|
||||||
.utility(false)
|
|
||||||
.constants(false)
|
|
||||||
.financial(false)
|
|
||||||
.conversion(false)
|
|
||||||
.register(&mut fresh_vm);
|
|
||||||
|
|
||||||
// Trigonometric functions should work
|
|
||||||
let result = fresh_vm.compile_and_run_raw_program(r#"(decimal-sin "0")"#.to_string());
|
|
||||||
assert!(result.is_ok());
|
|
||||||
|
|
||||||
let result = fresh_vm.compile_and_run_raw_program(r#"(decimal-cos "0")"#.to_string());
|
|
||||||
assert!(result.is_ok());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
fn test_comparison_only(mut fresh_vm: Engine) {
|
|
||||||
FunctionRegistryBuilder::new()
|
|
||||||
.basic_arithmetic(false)
|
|
||||||
.advanced_math(false)
|
|
||||||
.trigonometric(false)
|
|
||||||
.comparison(true)
|
|
||||||
.utility(false)
|
|
||||||
.constants(false)
|
|
||||||
.financial(false)
|
|
||||||
.conversion(false)
|
|
||||||
.register(&mut fresh_vm);
|
|
||||||
|
|
||||||
// Comparison functions should work
|
|
||||||
let result = fresh_vm.compile_and_run_raw_program(r#"(decimal-gt "5" "3")"#.to_string());
|
|
||||||
assert!(result.is_ok());
|
|
||||||
|
|
||||||
let result = fresh_vm.compile_and_run_raw_program(r#"(decimal-eq "5" "5")"#.to_string());
|
|
||||||
assert!(result.is_ok());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
fn test_utility_only(mut fresh_vm: Engine) {
|
|
||||||
FunctionRegistryBuilder::new()
|
|
||||||
.basic_arithmetic(false)
|
|
||||||
.advanced_math(false)
|
|
||||||
.trigonometric(false)
|
|
||||||
.comparison(false)
|
|
||||||
.utility(true)
|
|
||||||
.constants(false)
|
|
||||||
.financial(false)
|
|
||||||
.conversion(false)
|
|
||||||
.register(&mut fresh_vm);
|
|
||||||
|
|
||||||
// Utility functions should work
|
|
||||||
let result = fresh_vm.compile_and_run_raw_program(r#"(decimal-abs "-5")"#.to_string());
|
|
||||||
assert!(result.is_ok());
|
|
||||||
|
|
||||||
let result = fresh_vm.compile_and_run_raw_program(r#"(decimal-min "3" "5")"#.to_string());
|
|
||||||
assert!(result.is_ok());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
fn test_constants_only(mut fresh_vm: Engine) {
|
|
||||||
FunctionRegistryBuilder::new()
|
|
||||||
.basic_arithmetic(false)
|
|
||||||
.advanced_math(false)
|
|
||||||
.trigonometric(false)
|
|
||||||
.comparison(false)
|
|
||||||
.utility(false)
|
|
||||||
.constants(true)
|
|
||||||
.financial(false)
|
|
||||||
.conversion(false)
|
|
||||||
.register(&mut fresh_vm);
|
|
||||||
|
|
||||||
// Constants should work
|
|
||||||
let result = fresh_vm.compile_and_run_raw_program(r#"(decimal-pi)"#.to_string());
|
|
||||||
assert!(result.is_ok());
|
|
||||||
|
|
||||||
let result = fresh_vm.compile_and_run_raw_program(r#"(decimal-zero)"#.to_string());
|
|
||||||
assert!(result.is_ok());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
fn test_financial_only(mut fresh_vm: Engine) {
|
|
||||||
FunctionRegistryBuilder::new()
|
|
||||||
.basic_arithmetic(false)
|
|
||||||
.advanced_math(false)
|
|
||||||
.trigonometric(false)
|
|
||||||
.comparison(false)
|
|
||||||
.utility(false)
|
|
||||||
.constants(false)
|
|
||||||
.financial(true)
|
|
||||||
.conversion(false)
|
|
||||||
.register(&mut fresh_vm);
|
|
||||||
|
|
||||||
// Financial functions should work
|
|
||||||
let result = fresh_vm.compile_and_run_raw_program(r#"(decimal-percentage "100" "10")"#.to_string());
|
|
||||||
assert!(result.is_ok());
|
|
||||||
|
|
||||||
let result = fresh_vm.compile_and_run_raw_program(r#"(decimal-compound "1000" "0.05" "1")"#.to_string());
|
|
||||||
assert!(result.is_ok());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
fn test_conversion_only(mut fresh_vm: Engine) {
|
|
||||||
FunctionRegistryBuilder::new()
|
|
||||||
.basic_arithmetic(false)
|
|
||||||
.advanced_math(false)
|
|
||||||
.trigonometric(false)
|
|
||||||
.comparison(false)
|
|
||||||
.utility(false)
|
|
||||||
.constants(false)
|
|
||||||
.financial(false)
|
|
||||||
.conversion(true)
|
|
||||||
.register(&mut fresh_vm);
|
|
||||||
|
|
||||||
// Conversion functions should work
|
|
||||||
let result = fresh_vm.compile_and_run_raw_program(r#"(to-decimal "123.45")"#.to_string());
|
|
||||||
assert!(result.is_ok());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test variable registration
|
|
||||||
#[rstest]
|
|
||||||
fn test_variable_registration(mut fresh_vm: Engine) {
|
|
||||||
let mut variables = HashMap::new();
|
|
||||||
variables.insert("x".to_string(), "10.5".to_string());
|
|
||||||
variables.insert("y".to_string(), "20.3".to_string());
|
|
||||||
variables.insert("name".to_string(), "test_value".to_string());
|
|
||||||
|
|
||||||
FunctionRegistryBuilder::new()
|
|
||||||
.basic_arithmetic(true)
|
|
||||||
.with_variables(variables)
|
|
||||||
.register(&mut fresh_vm);
|
|
||||||
|
|
||||||
// Test getting variables
|
|
||||||
let result = fresh_vm.compile_and_run_raw_program(r#"(get-var "x")"#.to_string()).unwrap();
|
|
||||||
assert_eq!(result.len(), 1);
|
|
||||||
if let SteelVal::StringV(s) = &result[0] {
|
|
||||||
assert_eq!(s.to_string(), "10.5");
|
|
||||||
}
|
|
||||||
|
|
||||||
let result = fresh_vm.compile_and_run_raw_program(r#"(get-var "y")"#.to_string()).unwrap();
|
|
||||||
assert_eq!(result.len(), 1);
|
|
||||||
if let SteelVal::StringV(s) = &result[0] {
|
|
||||||
assert_eq!(s.to_string(), "20.3");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test checking if variables exist
|
|
||||||
let result = fresh_vm.compile_and_run_raw_program(r#"(has-var? "x")"#.to_string()).unwrap();
|
|
||||||
assert_eq!(result.len(), 1);
|
|
||||||
if let SteelVal::BoolV(b) = &result[0] {
|
|
||||||
assert!(b);
|
|
||||||
}
|
|
||||||
|
|
||||||
let result = fresh_vm.compile_and_run_raw_program(r#"(has-var? "nonexistent")"#.to_string()).unwrap();
|
|
||||||
assert_eq!(result.len(), 1);
|
|
||||||
if let SteelVal::BoolV(b) = &result[0] {
|
|
||||||
assert!(!b);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test using variables in arithmetic
|
|
||||||
let result = fresh_vm.compile_and_run_raw_program(r#"(decimal-add (get-var "x") (get-var "y"))"#.to_string()).unwrap();
|
|
||||||
assert_eq!(result.len(), 1);
|
|
||||||
if let SteelVal::StringV(s) = &result[0] {
|
|
||||||
assert_eq!(s.to_string(), "30.8");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test error handling in variable access
|
|
||||||
#[rstest]
|
|
||||||
fn test_variable_error_handling(mut fresh_vm: Engine) {
|
|
||||||
let variables = HashMap::new(); // Empty variables
|
|
||||||
|
|
||||||
FunctionRegistryBuilder::new()
|
|
||||||
.with_variables(variables)
|
|
||||||
.register(&mut fresh_vm);
|
|
||||||
|
|
||||||
// Should get an error for non-existent variable
|
|
||||||
let result = fresh_vm.compile_and_run_raw_program(r#"(get-var "nonexistent")"#.to_string());
|
|
||||||
// The function should return an error, but Steel might handle it differently
|
|
||||||
// This test ensures the function is registered and callable
|
|
||||||
assert!(result.is_ok() || result.is_err()); // Either way is fine, just shouldn't panic
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test function name listing
|
|
||||||
#[rstest]
|
|
||||||
fn test_function_names() {
|
|
||||||
let names = FunctionRegistry::get_function_names();
|
|
||||||
|
|
||||||
// Check that all expected function categories are present
|
|
||||||
assert!(names.contains(&"decimal-add"));
|
|
||||||
assert!(names.contains(&"decimal-pow"));
|
|
||||||
assert!(names.contains(&"decimal-sin"));
|
|
||||||
assert!(names.contains(&"decimal-gt"));
|
|
||||||
assert!(names.contains(&"decimal-abs"));
|
|
||||||
assert!(names.contains(&"decimal-pi"));
|
|
||||||
assert!(names.contains(&"decimal-percentage"));
|
|
||||||
assert!(names.contains(&"to-decimal"));
|
|
||||||
assert!(names.contains(&"get-var"));
|
|
||||||
assert!(names.contains(&"has-var?"));
|
|
||||||
|
|
||||||
// Check total count is reasonable
|
|
||||||
assert!(names.len() >= 25); // Should have at least 25 functions
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test builder pattern combinations
|
|
||||||
#[rstest]
|
|
||||||
fn test_builder_combinations(mut fresh_vm: Engine) {
|
|
||||||
FunctionRegistryBuilder::new()
|
|
||||||
.basic_arithmetic(true)
|
|
||||||
.comparison(true)
|
|
||||||
.constants(true)
|
|
||||||
.register(&mut fresh_vm);
|
|
||||||
|
|
||||||
// Should be able to use arithmetic, comparison, and constants
|
|
||||||
let result = fresh_vm.compile_and_run_raw_program(
|
|
||||||
r#"(decimal-gt (decimal-add "1" "2") (decimal-zero))"#.to_string()
|
|
||||||
).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(result.len(), 1);
|
|
||||||
if let SteelVal::BoolV(b) = &result[0] {
|
|
||||||
assert!(b); // 3 > 0 should be true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test default builder behavior
|
|
||||||
#[rstest]
|
|
||||||
fn test_builder_defaults(mut fresh_vm: Engine) {
|
|
||||||
// Default should include everything
|
|
||||||
FunctionRegistryBuilder::new().register(&mut fresh_vm);
|
|
||||||
|
|
||||||
// Should be able to use any function
|
|
||||||
let scripts = vec![
|
|
||||||
r#"(decimal-add "1" "2")"#,
|
|
||||||
r#"(decimal-pow "2" "3")"#,
|
|
||||||
r#"(decimal-sin "0")"#,
|
|
||||||
r#"(decimal-gt "5" "3")"#,
|
|
||||||
r#"(decimal-abs "-5")"#,
|
|
||||||
r#"(decimal-pi)"#,
|
|
||||||
r#"(decimal-percentage "100" "10")"#,
|
|
||||||
r#"(to-decimal "123.45")"#,
|
|
||||||
];
|
|
||||||
|
|
||||||
for script in scripts {
|
|
||||||
let result = fresh_vm.compile_and_run_raw_program(script.to_string());
|
|
||||||
assert!(result.is_ok(), "Failed to execute with default builder: {}", script);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test multiple variable sets
|
|
||||||
#[rstest]
|
|
||||||
fn test_multiple_variable_registration(mut fresh_vm: Engine) {
|
|
||||||
let mut variables1 = HashMap::new();
|
|
||||||
variables1.insert("a".to_string(), "1".to_string());
|
|
||||||
variables1.insert("b".to_string(), "2".to_string());
|
|
||||||
|
|
||||||
// Register first set
|
|
||||||
FunctionRegistryBuilder::new()
|
|
||||||
.with_variables(variables1)
|
|
||||||
.register(&mut fresh_vm);
|
|
||||||
|
|
||||||
// Test first set works
|
|
||||||
let result = fresh_vm.compile_and_run_raw_program(r#"(get-var "a")"#.to_string()).unwrap();
|
|
||||||
if let SteelVal::StringV(s) = &result[0] {
|
|
||||||
assert_eq!(s.to_string(), "1");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note: In a real scenario, you probably wouldn't register variables twice
|
|
||||||
// This is just testing that the registration mechanism works
|
|
||||||
}
|
|
||||||
@@ -1,439 +0,0 @@
|
|||||||
// tests/security_tests.rs
|
|
||||||
use rstest::*;
|
|
||||||
use steel_decimal::*;
|
|
||||||
use steel::steel_vm::engine::Engine;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
// Test stack overflow protection with deeply nested expressions
|
|
||||||
#[rstest]
|
|
||||||
fn test_stack_overflow_protection() {
|
|
||||||
let parser = ScriptParser::new();
|
|
||||||
|
|
||||||
// Create extremely deep nesting (potential stack overflow)
|
|
||||||
let mut expr = "1".to_string();
|
|
||||||
for i in 0..10000 {
|
|
||||||
expr = format!("(+ {} {})", expr, i);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Should not crash the process
|
|
||||||
let result = std::panic::catch_unwind(|| {
|
|
||||||
parser.transform(&expr)
|
|
||||||
});
|
|
||||||
|
|
||||||
// Either succeeds or panics gracefully, but shouldn't segfault
|
|
||||||
match result {
|
|
||||||
Ok(_) => {}, // Transformation succeeded
|
|
||||||
Err(_) => {}, // Panic caught, which is acceptable
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test memory exhaustion protection
|
|
||||||
#[rstest]
|
|
||||||
fn test_memory_exhaustion_protection() {
|
|
||||||
let parser = ScriptParser::new();
|
|
||||||
|
|
||||||
// Create expression designed to consume lots of memory
|
|
||||||
let large_var_name = "x".repeat(1_000_000); // 1MB variable name
|
|
||||||
let expr = format!("(+ ${} 1)", large_var_name);
|
|
||||||
|
|
||||||
// Should not consume unlimited memory
|
|
||||||
let result = std::panic::catch_unwind(|| {
|
|
||||||
parser.transform(&expr)
|
|
||||||
});
|
|
||||||
|
|
||||||
// Should handle gracefully
|
|
||||||
assert!(result.is_ok());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test injection attacks through variable names
|
|
||||||
#[rstest]
|
|
||||||
#[case("'; DROP TABLE users; --")] // SQL injection style
|
|
||||||
#[case("$(rm -rf /)")] // Shell injection style
|
|
||||||
#[case("<script>alert('xss')</script>")] // XSS style
|
|
||||||
#[case("../../etc/passwd")] // Path traversal style
|
|
||||||
#[case("${system('rm -rf /')}")] // Template injection style
|
|
||||||
#[case("{{7*7}}")] // Template injection
|
|
||||||
#[case("__proto__")] // Prototype pollution
|
|
||||||
#[case("constructor")] // Constructor pollution
|
|
||||||
#[case("\\x00\\x01\\x02")] // Null bytes and control chars
|
|
||||||
fn test_variable_name_injection(#[case] malicious_var: &str) {
|
|
||||||
let parser = ScriptParser::new();
|
|
||||||
// Attempt injection through variable name
|
|
||||||
let expr = format!("(+ ${} 1)", malicious_var);
|
|
||||||
let transformed = parser.transform(&expr);
|
|
||||||
|
|
||||||
// Should transform without executing malicious code
|
|
||||||
assert!(transformed.contains("get-var"));
|
|
||||||
|
|
||||||
// Extract what the parser actually captured as the variable name
|
|
||||||
let deps = parser.extract_dependencies(&expr);
|
|
||||||
assert!(!deps.is_empty(), "Should extract at least one dependency");
|
|
||||||
|
|
||||||
// The captured variable name should be in the transformed output
|
|
||||||
let captured_var = deps.iter().next().unwrap();
|
|
||||||
assert!(transformed.contains(captured_var));
|
|
||||||
|
|
||||||
// Security check: For inputs with dangerous characters (spaces, parens),
|
|
||||||
// verify that the parser truncated the variable name safely
|
|
||||||
if malicious_var.contains(' ') || malicious_var.contains('(') || malicious_var.contains(')') {
|
|
||||||
// Variable should be truncated, not the full malicious string
|
|
||||||
assert_ne!(captured_var, malicious_var,
|
|
||||||
"Parser should truncate variable names with dangerous characters");
|
|
||||||
assert!(!transformed.contains(malicious_var),
|
|
||||||
"Full malicious string should not appear in transformed output");
|
|
||||||
} else {
|
|
||||||
// If no dangerous characters, full variable name should be preserved
|
|
||||||
assert_eq!(captured_var, malicious_var);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test malicious Steel expressions
|
|
||||||
#[rstest]
|
|
||||||
#[case("(eval '(system \"rm -rf /\"))")] // Code execution attempt
|
|
||||||
#[case("(load \"../../etc/passwd\")")] // File access attempt
|
|
||||||
#[case("(require 'os) (os/execute \"malicious-command\")")] // Module injection
|
|
||||||
#[case("(define loop (lambda () (loop))) (loop)")] // Infinite recursion
|
|
||||||
#[case("(define mem-bomb (lambda () (cons 1 (mem-bomb)))) (mem-bomb)")] // Memory bomb
|
|
||||||
fn test_malicious_steel_expressions(#[case] malicious_expr: &str) {
|
|
||||||
let steel_decimal = SteelDecimal::new();
|
|
||||||
|
|
||||||
// Should not execute malicious Steel code during transformation
|
|
||||||
let transformed = steel_decimal.transform(malicious_expr);
|
|
||||||
|
|
||||||
// Transformation should complete without side effects
|
|
||||||
assert!(!transformed.is_empty());
|
|
||||||
|
|
||||||
// Should not contain the original malicious functions if transformed
|
|
||||||
if malicious_expr.contains("eval") || malicious_expr.contains("load") {
|
|
||||||
// These shouldn't be transformed into decimal operations
|
|
||||||
assert!(!transformed.contains("decimal-"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test parser regex exploitation
|
|
||||||
#[rstest]
|
|
||||||
#[case("((((((((((a")] // Unbalanced parentheses
|
|
||||||
fn test_parser_regex_exploitation_simple(#[case] malicious_input: &str) {
|
|
||||||
let parser = ScriptParser::new();
|
|
||||||
|
|
||||||
// Should not hang or consume excessive CPU
|
|
||||||
let start = std::time::Instant::now();
|
|
||||||
let result = std::panic::catch_unwind(|| {
|
|
||||||
parser.transform(malicious_input)
|
|
||||||
});
|
|
||||||
let duration = start.elapsed();
|
|
||||||
|
|
||||||
// Should complete within reasonable time (not ReDoS)
|
|
||||||
assert!(duration.as_secs() < 5, "Parser took too long: {:?}", duration);
|
|
||||||
|
|
||||||
// Should not crash
|
|
||||||
assert!(result.is_ok());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
fn test_parser_regex_exploitation_large_inputs() {
|
|
||||||
let parser = ScriptParser::new();
|
|
||||||
|
|
||||||
// Test extremely long variable reference
|
|
||||||
let large_var = format!("${}", "a".repeat(100000));
|
|
||||||
let start = std::time::Instant::now();
|
|
||||||
let result = std::panic::catch_unwind(|| {
|
|
||||||
parser.transform(&large_var)
|
|
||||||
});
|
|
||||||
let duration = start.elapsed();
|
|
||||||
assert!(duration.as_secs() < 5, "Large variable parsing took too long: {:?}", duration);
|
|
||||||
assert!(result.is_ok());
|
|
||||||
|
|
||||||
// Test repeated operators
|
|
||||||
let repeated_ops = format!("({}{})", "+".repeat(100000), " 1 2)");
|
|
||||||
let start = std::time::Instant::now();
|
|
||||||
let result = std::panic::catch_unwind(|| {
|
|
||||||
parser.transform(&repeated_ops)
|
|
||||||
});
|
|
||||||
let duration = start.elapsed();
|
|
||||||
assert!(duration.as_secs() < 5, "Repeated operators parsing took too long: {:?}", duration);
|
|
||||||
assert!(result.is_ok());
|
|
||||||
|
|
||||||
// Test huge string literals
|
|
||||||
let huge_string = format!("\"{}\"", "a".repeat(1000000));
|
|
||||||
let start = std::time::Instant::now();
|
|
||||||
let result = std::panic::catch_unwind(|| {
|
|
||||||
parser.transform(&huge_string)
|
|
||||||
});
|
|
||||||
let duration = start.elapsed();
|
|
||||||
assert!(duration.as_secs() < 5, "Huge string parsing took too long: {:?}", duration);
|
|
||||||
assert!(result.is_ok());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test Steel VM security integration
|
|
||||||
#[rstest]
|
|
||||||
fn test_steel_vm_security_integration() {
|
|
||||||
let mut vm = Engine::new();
|
|
||||||
let steel_decimal = SteelDecimal::new();
|
|
||||||
steel_decimal.register_functions(&mut vm);
|
|
||||||
|
|
||||||
// Test that we can't escape decimal functions to execute arbitrary code
|
|
||||||
let malicious_scripts = vec![
|
|
||||||
r#"(eval "(system \"echo pwned\")")"#,
|
|
||||||
r#"(load "../../etc/passwd")"#,
|
|
||||||
r#"(define dangerous (lambda () (system "rm -rf /")))"#,
|
|
||||||
r#"(require 'steel/core)"#, // Try to access core modules
|
|
||||||
];
|
|
||||||
|
|
||||||
for script in malicious_scripts {
|
|
||||||
let result = vm.compile_and_run_raw_program(script.to_string());
|
|
||||||
|
|
||||||
// These should fail to compile or execute, not succeed
|
|
||||||
match result {
|
|
||||||
Ok(_) => {
|
|
||||||
// If it succeeds, verify it didn't do anything dangerous
|
|
||||||
// (We can't really test this without side effects, so we assume it's safe)
|
|
||||||
}
|
|
||||||
Err(_) => {
|
|
||||||
// Expected - should fail to execute dangerous code
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test variable access security
|
|
||||||
#[rstest]
|
|
||||||
fn test_variable_access_security() {
|
|
||||||
let mut variables = HashMap::new();
|
|
||||||
variables.insert("safe_var".to_string(), "42".to_string());
|
|
||||||
variables.insert("password".to_string(), "secret123".to_string());
|
|
||||||
variables.insert("api_key".to_string(), "key_abc123".to_string());
|
|
||||||
|
|
||||||
let mut vm = Engine::new();
|
|
||||||
FunctionRegistry::register_variables(&mut vm, variables);
|
|
||||||
|
|
||||||
// Test that we can't enumerate all variables
|
|
||||||
let enumeration_attempts = vec![
|
|
||||||
r#"(map get-var (list "password" "api_key" "secret"))"#,
|
|
||||||
r#"(get-var "")"#, // Empty variable name
|
|
||||||
r#"(get-var nil)"#, // Nil variable name
|
|
||||||
];
|
|
||||||
|
|
||||||
for attempt in enumeration_attempts {
|
|
||||||
let result = vm.compile_and_run_raw_program(attempt.to_string());
|
|
||||||
// Should either fail or not reveal sensitive information
|
|
||||||
match result {
|
|
||||||
Ok(_) => {}, // If succeeds, assume it's safe
|
|
||||||
Err(_) => {}, // Expected failure
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test format string attacks through decimal formatting
|
|
||||||
#[rstest]
|
|
||||||
#[case("%s%s%s%s")] // Format string attack
|
|
||||||
#[case("%n")] // Write to memory attempt
|
|
||||||
#[case("%x%x%x%x")] // Memory reading attempt
|
|
||||||
#[case("\\x41\\x41\\x41\\x41")] // Buffer overflow attempt
|
|
||||||
fn test_format_string_attacks(#[case] format_attack: &str) {
|
|
||||||
// Test in various contexts where user input might be formatted
|
|
||||||
let _ = to_decimal(format_attack.to_string());
|
|
||||||
let _ = decimal_add(format_attack.to_string(), "1".to_string());
|
|
||||||
let _ = decimal_format("123.456".to_string(), 2); // Shouldn't use user input as format
|
|
||||||
|
|
||||||
// Should not crash or leak memory
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test buffer overflow attempts
|
|
||||||
#[rstest]
|
|
||||||
fn test_buffer_overflow_attempts() {
|
|
||||||
// Test with very long inputs that might cause buffer overflows in C libraries
|
|
||||||
let long_input = "A".repeat(100_000);
|
|
||||||
let long_number = "1".repeat(10_000) + "." + &"2".repeat(10_000);
|
|
||||||
|
|
||||||
// Should handle gracefully without buffer overflows
|
|
||||||
let _ = to_decimal(long_input);
|
|
||||||
let _ = to_decimal(long_number.clone());
|
|
||||||
let _ = decimal_add(long_number.clone(), "1".to_string());
|
|
||||||
let _ = decimal_sqrt(long_number);
|
|
||||||
|
|
||||||
// If we get here without crashing, buffer overflow protection works
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test denial of service through resource exhaustion
|
|
||||||
#[rstest]
|
|
||||||
fn test_resource_exhaustion_protection() {
|
|
||||||
let steel_decimal = SteelDecimal::new();
|
|
||||||
|
|
||||||
// Test CPU exhaustion
|
|
||||||
let cpu_bomb = "(+ ".repeat(10000) + "1" + &")".repeat(10000);
|
|
||||||
let start = std::time::Instant::now();
|
|
||||||
let _ = steel_decimal.transform(&cpu_bomb);
|
|
||||||
let duration = start.elapsed();
|
|
||||||
|
|
||||||
// Should not take excessive time
|
|
||||||
assert!(duration.as_secs() < 10, "CPU exhaustion detected");
|
|
||||||
|
|
||||||
// Test memory exhaustion through many variables
|
|
||||||
let mut steel_decimal = SteelDecimal::new();
|
|
||||||
for i in 0..100_000 {
|
|
||||||
steel_decimal.add_variable(format!("var_{}", i), "1".to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Should handle many variables without exhausting memory
|
|
||||||
let expr = "(+ $var_0 $var_99999)";
|
|
||||||
let _ = steel_decimal.transform(expr);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test integer overflow/underflow in precision settings
|
|
||||||
#[rstest]
|
|
||||||
#[case(u32::MAX)]
|
|
||||||
#[case(u32::MAX - 1)]
|
|
||||||
fn test_integer_overflow_in_precision(#[case] overflow_value: u32) {
|
|
||||||
// Should handle overflow gracefully
|
|
||||||
let result = set_precision(overflow_value);
|
|
||||||
assert!(result.contains("Error") || result.contains("Maximum"));
|
|
||||||
|
|
||||||
// Should not set invalid precision
|
|
||||||
let current = get_precision();
|
|
||||||
assert_ne!(current, overflow_value.to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test race conditions in precision settings (security through thread safety)
|
|
||||||
#[rstest]
|
|
||||||
fn test_precision_race_conditions() {
|
|
||||||
use std::sync::{Arc, Barrier};
|
|
||||||
use std::thread;
|
|
||||||
|
|
||||||
let num_threads = 10;
|
|
||||||
let barrier = Arc::new(Barrier::new(num_threads));
|
|
||||||
let success_count = Arc::new(std::sync::atomic::AtomicU32::new(0));
|
|
||||||
|
|
||||||
let handles: Vec<_> = (0..num_threads)
|
|
||||||
.map(|thread_id| {
|
|
||||||
let barrier = barrier.clone();
|
|
||||||
let success_count = success_count.clone();
|
|
||||||
|
|
||||||
thread::spawn(move || {
|
|
||||||
barrier.wait();
|
|
||||||
|
|
||||||
// Try to cause race condition
|
|
||||||
for i in 0..1000 {
|
|
||||||
let precision = (thread_id + i) % 5;
|
|
||||||
set_precision(precision as u32);
|
|
||||||
|
|
||||||
// Immediately use precision
|
|
||||||
let result = decimal_add("1.123456789".to_string(), "2.987654321".to_string());
|
|
||||||
if result.is_ok() {
|
|
||||||
success_count.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
for handle in handles {
|
|
||||||
handle.join().unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Should have high success rate (race conditions would cause failures)
|
|
||||||
let successes = success_count.load(std::sync::atomic::Ordering::Relaxed);
|
|
||||||
assert!(successes > (num_threads * 900) as u32, "Too many race condition failures: {}", successes);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test SQL injection style attacks through numeric inputs
|
|
||||||
#[rstest]
|
|
||||||
#[case("1; DROP TABLE decimals; --")]
|
|
||||||
#[case("1' OR '1'='1")]
|
|
||||||
#[case("1 UNION SELECT * FROM passwords")]
|
|
||||||
#[case("1; exec('rm -rf /')")]
|
|
||||||
fn test_sql_injection_style_attacks(#[case] injection_attempt: &str) {
|
|
||||||
// These should be treated as invalid decimal formats
|
|
||||||
let result = to_decimal(injection_attempt.to_string());
|
|
||||||
assert!(result.is_err(), "SQL injection attempt should fail: {}", injection_attempt);
|
|
||||||
|
|
||||||
// Should also fail in arithmetic
|
|
||||||
let add_result = decimal_add(injection_attempt.to_string(), "1".to_string());
|
|
||||||
assert!(add_result.is_err(), "Arithmetic with injection should fail");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test path traversal through variable names
|
|
||||||
#[rstest]
|
|
||||||
#[case("../../../etc/passwd")]
|
|
||||||
#[case("..\\..\\..\\windows\\system32\\config\\sam")]
|
|
||||||
#[case("/etc/passwd")]
|
|
||||||
#[case("C:\\Windows\\System32\\config\\SAM")]
|
|
||||||
#[case("file:///etc/passwd")]
|
|
||||||
#[case("data:text/plain;base64,cm9vdDp4OjA6MA==")]
|
|
||||||
fn test_path_traversal_attacks(#[case] path_attack: &str) {
|
|
||||||
let mut steel_decimal = SteelDecimal::new();
|
|
||||||
|
|
||||||
// Should treat as normal variable name, not file path
|
|
||||||
steel_decimal.add_variable(path_attack.to_string(), "42".to_string());
|
|
||||||
|
|
||||||
let expr = format!("(+ ${} 1)", path_attack);
|
|
||||||
let transformed = steel_decimal.transform(&expr);
|
|
||||||
|
|
||||||
// Should treat as variable reference, not attempt file access
|
|
||||||
assert!(transformed.contains("get-var"));
|
|
||||||
assert!(transformed.contains(path_attack));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test XML/HTML injection through variable values
|
|
||||||
#[rstest]
|
|
||||||
#[case("<xml><malicious>content</malicious></xml>")]
|
|
||||||
#[case("<!DOCTYPE html><script>alert('xss')</script>")]
|
|
||||||
#[case("<?xml version=\"1.0\"?><!DOCTYPE root [<!ENTITY test SYSTEM 'file:///etc/passwd'>]>")]
|
|
||||||
fn test_xml_html_injection(#[case] xml_attack: &str) {
|
|
||||||
let mut steel_decimal = SteelDecimal::new();
|
|
||||||
|
|
||||||
// Should treat as string value, not parse as XML/HTML
|
|
||||||
steel_decimal.add_variable("test_var".to_string(), xml_attack.to_string());
|
|
||||||
|
|
||||||
let vars = steel_decimal.get_variables();
|
|
||||||
assert_eq!(vars.get("test_var").unwrap(), xml_attack);
|
|
||||||
|
|
||||||
// Should not interpret as markup
|
|
||||||
assert!(!xml_attack.is_empty()); // Basic sanity check
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test deserialization attacks
|
|
||||||
#[rstest]
|
|
||||||
fn test_deserialization_attacks() {
|
|
||||||
// Test with serialized data that might trigger deserialization vulnerabilities
|
|
||||||
let malicious_serialized = vec![
|
|
||||||
"rO0ABXNyABFqYXZhLnV0aWwuSGFzaE1hcAUH2sHDFmDRAwACRgAKbG9hZEZhY3RvckkACXRocmVzaG9sZHhwP0AAAAAAAAx3CAAAABAAAAABdAABYXQAAWJ4",
|
|
||||||
"AC ED 00 05 73 72",
|
|
||||||
"pickle\\x80\\x03]q\\x00.",
|
|
||||||
];
|
|
||||||
|
|
||||||
for payload in malicious_serialized {
|
|
||||||
// Should treat as regular string, not attempt deserialization
|
|
||||||
let result = to_decimal(payload.to_string());
|
|
||||||
assert!(result.is_err(), "Serialized payload should not be valid decimal");
|
|
||||||
|
|
||||||
let mut steel_decimal = SteelDecimal::new();
|
|
||||||
steel_decimal.add_variable("payload".to_string(), payload.to_string());
|
|
||||||
|
|
||||||
// Should store as string value
|
|
||||||
assert_eq!(steel_decimal.get_variables().get("payload").unwrap(), payload);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test timing attacks
|
|
||||||
#[rstest]
|
|
||||||
fn test_timing_attack_resistance() {
|
|
||||||
// Test that comparison operations don't leak information through timing
|
|
||||||
let values = vec!["1", "1.0", "1.00", "1.000"];
|
|
||||||
let mut times = Vec::new();
|
|
||||||
|
|
||||||
for value in values {
|
|
||||||
let start = std::time::Instant::now();
|
|
||||||
let _ = decimal_eq(value.to_string(), "1".to_string());
|
|
||||||
let duration = start.elapsed();
|
|
||||||
times.push(duration);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Times should be relatively similar (not vulnerable to timing attacks)
|
|
||||||
let max_time = times.iter().max().unwrap();
|
|
||||||
let min_time = times.iter().min().unwrap();
|
|
||||||
let ratio = max_time.as_nanos() as f64 / min_time.as_nanos() as f64;
|
|
||||||
|
|
||||||
// Allow for reasonable variance but not massive differences
|
|
||||||
assert!(ratio < 10.0, "Timing attack vulnerability detected: ratio = {}", ratio);
|
|
||||||
}
|
|
||||||
@@ -1,330 +0,0 @@
|
|||||||
use rstest::*;
|
|
||||||
use steel_decimal::{TypeConverter, ScriptAnalyzer, DecimalPrecision, ConversionError};
|
|
||||||
use steel::rvals::SteelVal;
|
|
||||||
use rust_decimal::Decimal;
|
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
// TypeConverter Tests
|
|
||||||
#[rstest]
|
|
||||||
#[case("123.456")]
|
|
||||||
#[case("42")]
|
|
||||||
#[case("-15.75")]
|
|
||||||
#[case("0")]
|
|
||||||
fn test_steel_val_string_to_decimal(#[case] input: &str) {
|
|
||||||
let steel_val = SteelVal::StringV(input.into());
|
|
||||||
let result = TypeConverter::steel_val_to_decimal(&steel_val).unwrap();
|
|
||||||
let expected = Decimal::from_str(input).unwrap();
|
|
||||||
assert_eq!(result, expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
#[case(42isize)]
|
|
||||||
#[case(-15isize)]
|
|
||||||
#[case(0isize)]
|
|
||||||
fn test_steel_val_int_to_decimal(#[case] input: isize) {
|
|
||||||
let steel_val = SteelVal::IntV(input);
|
|
||||||
let result = TypeConverter::steel_val_to_decimal(&steel_val).unwrap();
|
|
||||||
let expected = Decimal::from(input);
|
|
||||||
assert_eq!(result, expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
#[case(42.5)]
|
|
||||||
#[case(-15.75)]
|
|
||||||
#[case(0.0)]
|
|
||||||
fn test_steel_val_num_to_decimal(#[case] input: f64) {
|
|
||||||
let steel_val = SteelVal::NumV(input);
|
|
||||||
let result = TypeConverter::steel_val_to_decimal(&steel_val).unwrap();
|
|
||||||
let expected = Decimal::try_from(input).unwrap();
|
|
||||||
assert_eq!(result, expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
fn test_steel_val_unsupported_type() {
|
|
||||||
let steel_val = SteelVal::BoolV(true);
|
|
||||||
let result = TypeConverter::steel_val_to_decimal(&steel_val);
|
|
||||||
assert!(result.is_err());
|
|
||||||
assert!(matches!(result.unwrap_err(), ConversionError::UnsupportedType(_)));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
fn test_steel_val_invalid_decimal_string() {
|
|
||||||
let steel_val = SteelVal::StringV("not_a_number".into());
|
|
||||||
let result = TypeConverter::steel_val_to_decimal(&steel_val);
|
|
||||||
assert!(result.is_err());
|
|
||||||
assert!(matches!(result.unwrap_err(), ConversionError::InvalidDecimal(_)));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
#[case("123.456")]
|
|
||||||
#[case("42")]
|
|
||||||
#[case("-15.75")]
|
|
||||||
#[case("0")]
|
|
||||||
fn test_decimal_to_steel_val(#[case] input: &str) {
|
|
||||||
let decimal = Decimal::from_str(input).unwrap();
|
|
||||||
let result = TypeConverter::decimal_to_steel_val(decimal);
|
|
||||||
|
|
||||||
if let SteelVal::StringV(s) = result {
|
|
||||||
assert_eq!(s.to_string(), input);
|
|
||||||
} else {
|
|
||||||
panic!("Expected StringV");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
fn test_steel_vals_to_strings() {
|
|
||||||
let vals = vec![
|
|
||||||
SteelVal::StringV("hello".into()),
|
|
||||||
SteelVal::IntV(42),
|
|
||||||
SteelVal::NumV(3.14),
|
|
||||||
SteelVal::BoolV(true),
|
|
||||||
];
|
|
||||||
|
|
||||||
let result = TypeConverter::steel_vals_to_strings(vals).unwrap();
|
|
||||||
let expected = vec!["hello", "42", "3.14", "true"];
|
|
||||||
assert_eq!(result, expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
#[case("123.456", "123.456")]
|
|
||||||
#[case("42", "42")]
|
|
||||||
#[case("-15.75", "-15.75")]
|
|
||||||
fn test_validate_decimal_string(#[case] input: &str, #[case] expected: &str) {
|
|
||||||
let result = TypeConverter::validate_decimal_string(input).unwrap();
|
|
||||||
assert_eq!(result, expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
#[case("not_a_number")]
|
|
||||||
#[case("")]
|
|
||||||
#[case("12.34.56")]
|
|
||||||
fn test_validate_decimal_string_invalid(#[case] input: &str) {
|
|
||||||
let result = TypeConverter::validate_decimal_string(input);
|
|
||||||
assert!(result.is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
#[case(123.456)]
|
|
||||||
#[case(-15.75)]
|
|
||||||
#[case(0.0)]
|
|
||||||
fn test_f64_to_decimal_string(#[case] input: f64) {
|
|
||||||
let result = TypeConverter::f64_to_decimal_string(input).unwrap();
|
|
||||||
let expected = Decimal::try_from(input).unwrap().to_string();
|
|
||||||
assert_eq!(result, expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
#[case(42i64, "42")]
|
|
||||||
#[case(-15i64, "-15")]
|
|
||||||
#[case(0i64, "0")]
|
|
||||||
fn test_i64_to_decimal_string(#[case] input: i64, #[case] expected: &str) {
|
|
||||||
let result = TypeConverter::i64_to_decimal_string(input);
|
|
||||||
assert_eq!(result, expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
#[case(42u64, "42")]
|
|
||||||
#[case(0u64, "0")]
|
|
||||||
#[case(1000u64, "1000")]
|
|
||||||
fn test_u64_to_decimal_string(#[case] input: u64, #[case] expected: &str) {
|
|
||||||
let result = TypeConverter::u64_to_decimal_string(input);
|
|
||||||
assert_eq!(result, expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ScriptAnalyzer Tests
|
|
||||||
#[rstest]
|
|
||||||
#[case("123.456", true)]
|
|
||||||
#[case("42", true)]
|
|
||||||
#[case("-15.75", true)]
|
|
||||||
#[case("0", true)]
|
|
||||||
#[case("not_a_number", false)]
|
|
||||||
#[case("", false)]
|
|
||||||
#[case("12.34.56", false)]
|
|
||||||
fn test_is_decimal_like(#[case] input: &str, #[case] expected: bool) {
|
|
||||||
let result = ScriptAnalyzer::is_decimal_like(input);
|
|
||||||
assert_eq!(result, expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
#[case(r#"(test "hello" "world")"#, vec!["hello", "world"])]
|
|
||||||
#[case(r#"(add "1.5" "2.3")"#, vec!["1.5", "2.3"])]
|
|
||||||
#[case(r#"(func)"#, vec![])]
|
|
||||||
#[case(r#""single""#, vec!["single"])]
|
|
||||||
fn test_extract_string_literals(#[case] input: &str, #[case] expected: Vec<&str>) {
|
|
||||||
let result = ScriptAnalyzer::extract_string_literals(input);
|
|
||||||
let expected: Vec<String> = expected.into_iter().map(|s| s.to_string()).collect();
|
|
||||||
assert_eq!(result, expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
#[case("(decimal-add x y)", "decimal-add", 1)]
|
|
||||||
#[case("(decimal-add x y) (decimal-add a b)", "decimal-add", 2)]
|
|
||||||
#[case("(decimal-mul x y)", "decimal-add", 0)]
|
|
||||||
#[case("(decimal-add x (decimal-add y z))", "decimal-add", 2)]
|
|
||||||
fn test_count_function_calls(#[case] script: &str, #[case] function_name: &str, #[case] expected: usize) {
|
|
||||||
let result = ScriptAnalyzer::count_function_calls(script, function_name);
|
|
||||||
assert_eq!(result, expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
#[case("(decimal-add x y)", true)]
|
|
||||||
#[case("(decimal-mul a b)", true)]
|
|
||||||
#[case("(decimal-sin x)", true)]
|
|
||||||
#[case("(regular-function x y)", false)]
|
|
||||||
#[case("(+ x y)", false)]
|
|
||||||
fn test_contains_decimal_functions(#[case] script: &str, #[case] expected: bool) {
|
|
||||||
let result = ScriptAnalyzer::contains_decimal_functions(script);
|
|
||||||
assert_eq!(result, expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
// DecimalPrecision Tests
|
|
||||||
#[rstest]
|
|
||||||
#[case("123.456789", 2, "123.46")]
|
|
||||||
#[case("123.456789", 4, "123.4568")]
|
|
||||||
#[case("123.456789", 0, "123")]
|
|
||||||
#[case("123", 2, "123")]
|
|
||||||
fn test_set_precision(#[case] input: &str, #[case] precision: u32, #[case] expected: &str) {
|
|
||||||
let result = DecimalPrecision::set_precision(input, precision).unwrap();
|
|
||||||
assert_eq!(result, expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
#[case("123.456", 3)]
|
|
||||||
#[case("123", 0)]
|
|
||||||
#[case("123.000", 3)]
|
|
||||||
#[case("0.001", 3)]
|
|
||||||
fn test_get_decimal_places(#[case] input: &str, #[case] expected: u32) {
|
|
||||||
let result = DecimalPrecision::get_decimal_places(input).unwrap();
|
|
||||||
assert_eq!(result, expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
#[case("123.000", "123")]
|
|
||||||
#[case("123.456", "123.456")]
|
|
||||||
#[case("0.100", "0.1")]
|
|
||||||
#[case("1000.000", "1000")]
|
|
||||||
fn test_normalize(#[case] input: &str, #[case] expected: &str) {
|
|
||||||
let result = DecimalPrecision::normalize(input).unwrap();
|
|
||||||
assert_eq!(result, expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
fn test_precision_invalid_input() {
|
|
||||||
let result = DecimalPrecision::set_precision("not_a_number", 2);
|
|
||||||
assert!(result.is_err());
|
|
||||||
|
|
||||||
let result = DecimalPrecision::get_decimal_places("invalid");
|
|
||||||
assert!(result.is_err());
|
|
||||||
|
|
||||||
let result = DecimalPrecision::normalize("bad_input");
|
|
||||||
assert!(result.is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Edge Cases and Error Handling
|
|
||||||
#[rstest]
|
|
||||||
fn test_type_converter_edge_cases() {
|
|
||||||
// Empty string
|
|
||||||
let result = TypeConverter::validate_decimal_string("");
|
|
||||||
assert!(result.is_err());
|
|
||||||
|
|
||||||
// Very large number
|
|
||||||
let large_num = "9999999999999999999999999999";
|
|
||||||
let result = TypeConverter::validate_decimal_string(large_num);
|
|
||||||
assert!(result.is_ok());
|
|
||||||
|
|
||||||
// Very small number
|
|
||||||
let small_num = "0.000000000000000000000001";
|
|
||||||
let result = TypeConverter::validate_decimal_string(small_num);
|
|
||||||
assert!(result.is_ok());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
fn test_script_analyzer_edge_cases() {
|
|
||||||
// Empty script
|
|
||||||
let result = ScriptAnalyzer::extract_string_literals("");
|
|
||||||
assert!(result.is_empty());
|
|
||||||
|
|
||||||
let result = ScriptAnalyzer::contains_decimal_functions("");
|
|
||||||
assert!(!result);
|
|
||||||
|
|
||||||
// Script with no functions
|
|
||||||
let result = ScriptAnalyzer::count_function_calls("just some text", "decimal-add");
|
|
||||||
assert_eq!(result, 0);
|
|
||||||
|
|
||||||
// Script with escaped quotes
|
|
||||||
let result = ScriptAnalyzer::extract_string_literals(r#"(test "hello \"world\"")"#);
|
|
||||||
assert_eq!(result, vec!["hello \\\"world\\\""]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
fn test_conversion_error_types() {
|
|
||||||
// Test InvalidDecimal error
|
|
||||||
let result = TypeConverter::validate_decimal_string("invalid");
|
|
||||||
assert!(result.is_err());
|
|
||||||
if let Err(ConversionError::InvalidDecimal(msg)) = result {
|
|
||||||
assert!(msg.contains("invalid"));
|
|
||||||
} else {
|
|
||||||
panic!("Expected InvalidDecimal error");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test UnsupportedType error
|
|
||||||
let steel_val = SteelVal::BoolV(true);
|
|
||||||
let result = TypeConverter::steel_val_to_decimal(&steel_val);
|
|
||||||
assert!(result.is_err());
|
|
||||||
assert!(matches!(result.unwrap_err(), ConversionError::UnsupportedType(_)));
|
|
||||||
|
|
||||||
// Test ConversionFailed error (using f64 that can't be converted)
|
|
||||||
let result = TypeConverter::f64_to_decimal_string(f64::NAN);
|
|
||||||
assert!(result.is_err());
|
|
||||||
if let Err(ConversionError::ConversionFailed(msg)) = result {
|
|
||||||
assert!(msg.contains("f64 to decimal"));
|
|
||||||
} else {
|
|
||||||
panic!("Expected ConversionFailed error");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Integration tests for utilities
|
|
||||||
#[rstest]
|
|
||||||
fn test_utility_integration() {
|
|
||||||
// Test a complete workflow: string -> decimal -> string
|
|
||||||
let input = "123.456789";
|
|
||||||
|
|
||||||
// Validate input
|
|
||||||
let validated = TypeConverter::validate_decimal_string(input).unwrap();
|
|
||||||
|
|
||||||
// Set precision
|
|
||||||
let precise = DecimalPrecision::set_precision(&validated, 2).unwrap();
|
|
||||||
assert_eq!(precise, "123.46");
|
|
||||||
|
|
||||||
// Check if it's decimal-like
|
|
||||||
assert!(ScriptAnalyzer::is_decimal_like(&precise));
|
|
||||||
|
|
||||||
// Normalize (should be no change since already precise)
|
|
||||||
let normalized = DecimalPrecision::normalize(&precise).unwrap();
|
|
||||||
assert_eq!(normalized, "123.46");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
fn test_complex_script_analysis() {
|
|
||||||
let script = r#"
|
|
||||||
(decimal-add "1.5" "2.3")
|
|
||||||
(decimal-mul "result" "factor")
|
|
||||||
(decimal-sin "angle")
|
|
||||||
(decimal-gt "value" "threshold")
|
|
||||||
"#;
|
|
||||||
|
|
||||||
// Should detect decimal functions
|
|
||||||
assert!(ScriptAnalyzer::contains_decimal_functions(script));
|
|
||||||
|
|
||||||
// Count specific functions
|
|
||||||
assert_eq!(ScriptAnalyzer::count_function_calls(script, "decimal-add"), 1);
|
|
||||||
assert_eq!(ScriptAnalyzer::count_function_calls(script, "decimal-mul"), 1);
|
|
||||||
assert_eq!(ScriptAnalyzer::count_function_calls(script, "decimal-sin"), 1);
|
|
||||||
assert_eq!(ScriptAnalyzer::count_function_calls(script, "decimal-gt"), 1);
|
|
||||||
|
|
||||||
// Extract string literals
|
|
||||||
let literals = ScriptAnalyzer::extract_string_literals(script);
|
|
||||||
assert!(literals.contains(&"1.5".to_string()));
|
|
||||||
assert!(literals.contains(&"2.3".to_string()));
|
|
||||||
assert!(literals.contains(&"result".to_string()));
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user