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:
filipriec
2025-07-08 18:02:32 +02:00
parent 433d87c96d
commit a3dd6fa95b
28 changed files with 61 additions and 5797 deletions

563
Cargo.lock generated
View File

@@ -118,18 +118,6 @@ dependencies = [
"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]]
name = "anyhow"
version = "1.0.98"
@@ -336,21 +324,6 @@ dependencies = [
"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]]
name = "bitflags"
version = "2.9.0"
@@ -503,12 +476,6 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
[[package]]
name = "cast"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
[[package]]
name = "castaway"
version = "0.2.3"
@@ -562,33 +529,6 @@ dependencies = [
"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]]
name = "cipher"
version = "0.4.4"
@@ -599,31 +539,6 @@ dependencies = [
"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]]
name = "client"
version = "0.3.13"
@@ -632,7 +547,7 @@ dependencies = [
"async-trait",
"common",
"crossterm",
"dirs 6.0.0",
"dirs",
"dotenvy",
"futures",
"lazy_static",
@@ -786,52 +701,6 @@ dependencies = [
"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]]
name = "crossbeam-channel"
version = "0.5.15"
@@ -986,12 +855,6 @@ dependencies = [
"serde",
]
[[package]]
name = "diff"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
[[package]]
name = "digest"
version = "0.10.7"
@@ -1004,34 +867,13 @@ dependencies = [
"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]]
name = "dirs"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
dependencies = [
"dirs-sys 0.5.0",
]
[[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",
"dirs-sys",
]
[[package]]
@@ -1042,7 +884,7 @@ checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
dependencies = [
"libc",
"option-ext",
"redox_users 0.5.0",
"redox_users",
"windows-sys 0.59.0",
]
@@ -1084,16 +926,6 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "equivalent"
version = "1.0.2"
@@ -1405,16 +1237,6 @@ dependencies = [
"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]]
name = "hashbrown"
version = "0.12.3"
@@ -1793,6 +1615,7 @@ dependencies = [
"bitmaps",
"rand_core 0.6.4",
"rand_xoshiro",
"serde",
"sized-chunks",
"typenum",
"version_check",
@@ -1816,6 +1639,7 @@ dependencies = [
"bitmaps",
"rand_core 0.6.4",
"rand_xoshiro",
"serde",
"sized-chunks",
"typenum",
"version_check",
@@ -1869,15 +1693,6 @@ dependencies = [
"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]]
name = "itertools"
version = "0.13.0"
@@ -2177,20 +1992,6 @@ dependencies = [
"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]]
name = "num-bigint"
version = "0.4.6"
@@ -2219,16 +2020,6 @@ dependencies = [
"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]]
name = "num-conv"
version = "0.1.0"
@@ -2264,7 +2055,6 @@ dependencies = [
"num-bigint",
"num-integer",
"num-traits",
"serde",
]
[[package]]
@@ -2307,12 +2097,6 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4ce411919553d3f9fa53a0880544cda985a112117a0444d5ff1e870a893d6ea"
[[package]]
name = "oorandom"
version = "11.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e"
[[package]]
name = "openssl"
version = "0.10.72"
@@ -2507,34 +2291,6 @@ version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "polling"
version = "3.7.4"
@@ -2576,16 +2332,6 @@ dependencies = [
"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]]
name = "prettyplease"
version = "0.2.32"
@@ -2636,26 +2382,6 @@ dependencies = [
"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]]
name = "prost"
version = "0.13.5"
@@ -2728,23 +2454,6 @@ dependencies = [
"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]]
name = "quickscope"
version = "0.2.0"
@@ -2776,12 +2485,6 @@ version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
[[package]]
name = "radix_fmt"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce082a9940a7ace2ad4a8b7d0b1eac6aa378895f18be598230c5f2284ac05426"
[[package]]
name = "rand"
version = "0.8.5"
@@ -2851,15 +2554,6 @@ dependencies = [
"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]]
name = "rand_xoshiro"
version = "0.6.0"
@@ -2919,17 +2613,6 @@ dependencies = [
"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]]
name = "redox_users"
version = "0.5.0"
@@ -2938,7 +2621,7 @@ checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b"
dependencies = [
"getrandom 0.2.15",
"libredox",
"thiserror 2.0.12",
"thiserror",
]
[[package]]
@@ -3176,42 +2859,12 @@ version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "ryu"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "schannel"
version = "0.1.27"
@@ -3227,12 +2880,6 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "sdd"
version = "3.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "584e070911c7017da6cb2eb0788d09f43d789029b5877d3e5ecc8acf86ceee21"
[[package]]
name = "seahash"
version = "4.1.0"
@@ -3338,31 +2985,6 @@ dependencies = [
"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]]
name = "server"
version = "0.3.13"
@@ -3389,10 +3011,10 @@ dependencies = [
"serde_json",
"sqlx",
"steel-core",
"steel-derive 0.5.0 (git+https://github.com/mattwparas/steel.git?branch=master)",
"steel_decimal",
"steel-decimal",
"steel-derive",
"tantivy",
"thiserror 2.0.12",
"thiserror",
"time",
"tokio",
"tonic",
@@ -3493,7 +3115,7 @@ checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb"
dependencies = [
"num-bigint",
"num-traits",
"thiserror 2.0.12",
"thiserror",
"time",
]
@@ -3606,7 +3228,7 @@ dependencies = [
"serde_json",
"sha2",
"smallvec",
"thiserror 2.0.12",
"thiserror",
"time",
"tokio",
"tokio-stream",
@@ -3692,7 +3314,7 @@ dependencies = [
"smallvec",
"sqlx-core",
"stringprep",
"thiserror 2.0.12",
"thiserror",
"time",
"tracing",
"uuid",
@@ -3733,7 +3355,7 @@ dependencies = [
"smallvec",
"sqlx-core",
"stringprep",
"thiserror 2.0.12",
"thiserror",
"time",
"tracing",
"uuid",
@@ -3760,7 +3382,7 @@ dependencies = [
"serde",
"serde_urlencoded",
"sqlx-core",
"thiserror 2.0.12",
"thiserror",
"time",
"tracing",
"url",
@@ -3781,8 +3403,9 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "steel-core"
version = "0.6.0"
source = "git+https://github.com/mattwparas/steel.git#2f28ab10523198726d343257d29d892864e897b0"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79fcd4a04c6b2fc5f695ad37b379ad0e4245a9a12dcd742fe4fe1ad15ddd6546"
dependencies = [
"abi_stable",
"anyhow",
@@ -3793,55 +3416,62 @@ dependencies = [
"chrono",
"codespan-reporting",
"compact_str",
"crossbeam",
"dirs 5.0.1",
"crossbeam-channel",
"crossbeam-utils",
"env_home",
"futures-executor",
"futures-task",
"futures-util",
"fxhash",
"getrandom 0.3.2",
"home",
"http",
"glob",
"httparse",
"im",
"im-lists",
"im-rc",
"lasso",
"log",
"num",
"md-5",
"num-bigint",
"num-integer",
"num-rational",
"num-traits",
"once_cell",
"parking_lot",
"polling",
"pretty",
"quickscope",
"radix_fmt",
"rand 0.9.1",
"serde",
"serde_derive",
"serde_json",
"smallvec",
"steel-derive 0.5.0 (git+https://github.com/mattwparas/steel.git)",
"steel-derive",
"steel-gen",
"steel-parser",
"strsim",
"weak-table",
"which",
"xdg",
]
[[package]]
name = "steel-derive"
version = "0.5.0"
source = "git+https://github.com/mattwparas/steel.git?branch=master#2f28ab10523198726d343257d29d892864e897b0"
name = "steel-decimal"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c43950a3eed43f3e9765a51f5dc1b0de5e1687ba824b8589990747d9ba241187"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.100",
"regex",
"rust_decimal",
"rust_decimal_macros",
"steel-core",
"steel-derive",
"thiserror",
]
[[package]]
name = "steel-derive"
version = "0.5.0"
source = "git+https://github.com/mattwparas/steel.git#2f28ab10523198726d343257d29d892864e897b0"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bab7d1c6e3ee7b3ba6a4187b545d7dc1fa6e4929f0721a8798cfc3b8c7ed2b23"
dependencies = [
"proc-macro2",
"quote",
@@ -3850,50 +3480,32 @@ dependencies = [
[[package]]
name = "steel-gen"
version = "0.2.0"
source = "git+https://github.com/mattwparas/steel.git#2f28ab10523198726d343257d29d892864e897b0"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "deabc06d4f4735677503f593ae185d3f47c0fa8089b7e9a7177e0e0544483d86"
dependencies = [
"codegen",
"serde",
"serde_derive",
]
[[package]]
name = "steel-parser"
version = "0.6.0"
source = "git+https://github.com/mattwparas/steel.git#2f28ab10523198726d343257d29d892864e897b0"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cf95d03e34e5d34a318a1f3ad7c933628c476776d3dc4fe9accb5b7b6b5a295"
dependencies = [
"compact_str",
"fxhash",
"lasso",
"num",
"num-bigint",
"num-rational",
"num-traits",
"once_cell",
"pretty",
"serde",
"serde_derive",
"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]]
name = "stringprep"
version = "0.1.5"
@@ -4024,7 +3636,7 @@ dependencies = [
"tantivy-stacker",
"tantivy-tokenizer-api",
"tempfile",
"thiserror 2.0.12",
"thiserror",
"time",
"uuid",
"winapi",
@@ -4152,33 +3764,13 @@ dependencies = [
"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]]
name = "thiserror"
version = "2.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
dependencies = [
"thiserror-impl 2.0.12",
]
[[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",
"thiserror-impl",
]
[[package]]
@@ -4245,16 +3837,6 @@ dependencies = [
"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]]
name = "tinyvec"
version = "1.9.0"
@@ -4560,12 +4142,6 @@ version = "1.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f"
[[package]]
name = "unarray"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94"
[[package]]
name = "unicode-bidi"
version = "0.3.18"
@@ -4717,25 +4293,6 @@ version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "want"
version = "0.3.1"
@@ -4830,16 +4387,6 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "which"
version = "7.0.3"
@@ -5146,10 +4693,10 @@ dependencies = [
]
[[package]]
name = "yansi"
version = "1.0.1"
name = "xdg"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
checksum = "2fb433233f2df9344722454bc7e96465c9d03bff9d77c248f9e7523fe79585b5"
[[package]]
name = "yoke"

View File

@@ -1,5 +1,5 @@
[workspace]
members = ["client", "server", "common", "search", "steel_decimal"]
members = ["client", "server", "common", "search"]
resolver = "2"
[workspace.package]

View File

@@ -7,7 +7,6 @@ license = "AGPL-3.0-or-later"
[dependencies]
common = { path = "../common" }
search = { path = "../search" }
steel_decimal = { path = "../steel_decimal" }
anyhow = { workspace = true }
tantivy = { workspace = true }
@@ -23,8 +22,10 @@ tonic = "0.13.0"
tonic-reflection = "0.13.0"
tracing = "0.1.41"
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"
lazy_static = "1.5.0"
bcrypt = "0.17.0"
@@ -37,6 +38,7 @@ rust_decimal = { workspace = true }
rust_decimal_macros = { workspace = true }
regex = { workspace = true }
thiserror = { workspace = true }
steel-decimal = "1.0.0"
[lib]
name = "server"

View File

@@ -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

View File

@@ -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"] }

View File

@@ -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.

View File

@@ -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.

View File

@@ -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

View File

@@ -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.

View File

@@ -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.

View File

@@ -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);
}
}
}

View File

@@ -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!();
}

View File

@@ -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);
}

View File

@@ -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))
}

View File

@@ -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};
}

View File

@@ -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: &regex::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: &regex::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()
}
}

View File

@@ -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()
}
}

View File

@@ -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())
}
}

View File

@@ -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
}
}
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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"));
}
}

View File

@@ -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\""));
}

View File

@@ -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"]

View File

@@ -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);
}

View File

@@ -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
}

View File

@@ -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);
}

View File

@@ -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()));
}