diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1c0addc..c8b93a1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,7 +38,12 @@ jobs: - name: Install system dependencies run: sudo apt-get install -y libopenblas-dev - + + - name: Install R + uses: r-lib/actions/setup-r@v2 + with: + r-version: release + - name: Install Rust uses: dtolnay/rust-toolchain@stable with: @@ -47,10 +52,10 @@ jobs: - uses: Swatinem/rust-cache@v2 - name: Run Clippy - run: cargo clippy --workspace --exclude ci_python --exclude ci_r --exclude ci_js --all-targets -- -D warnings + run: cargo clippy --workspace --exclude ci_python --exclude ci_js --all-targets -- -D warnings test: - name: Test Suite + name: Rust Tests needs: [fmt, clippy] runs-on: ${{ matrix.os }} strategy: @@ -76,7 +81,43 @@ jobs: - uses: Swatinem/rust-cache@v2 - name: Build - run: cargo build --workspace --exclude ci_python --exclude ci_r --exclude ci_js --verbose + run: cargo build --workspace --exclude ci_python --exclude cir --exclude ci_js --verbose - name: Run tests - run: cargo test --workspace --exclude ci_python --exclude ci_r --exclude ci_js --verbose + run: cargo test --workspace --exclude ci_python --exclude cir --exclude ci_js --verbose + + r-test: + name: R Package Tests + needs: [fmt, clippy] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + + - name: Install system dependencies + run: sudo apt-get install -y libopenblas-dev + + - name: Install R + uses: r-lib/actions/setup-r@v2 + with: + r-version: release + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + + - uses: Swatinem/rust-cache@v2 + + - name: Install R package dependencies + uses: r-lib/actions/setup-r-dependencies@v2 + with: + working-directory: crates/cir + extra-packages: any::devtools, any::rextendr + + - name: Generate Makevars and R wrappers + working-directory: crates/cir + run: rextendr::document() + shell: Rscript {0} + + - name: Run R tests + working-directory: crates/cir + run: devtools::test(reporter = "summary") + shell: Rscript {0} diff --git a/Cargo.lock b/Cargo.lock index be46ed0..93463ca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -94,9 +94,9 @@ checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" [[package]] name = "bitflags" -version = "2.11.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" [[package]] name = "block-buffer" @@ -139,7 +139,7 @@ checksum = "9ff11ddd2af3b5e80dd0297fee6e56ac038d9bdc549573cdb51bd6d2efe7f05e" dependencies = [ "num-complex", "num-traits", - "rand 0.8.5", + "rand 0.8.6", "serde", ] @@ -154,9 +154,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.59" +version = "1.2.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7a4d3ec6524d28a329fc53654bbadc9bdd7b0431f5d65f1a56ffb28a1ee5283" +checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20" dependencies = [ "find-msvc-tools", "shlex", @@ -192,7 +192,7 @@ dependencies = [ "ndarray-linalg", "ordered-float", "proptest", - "rand 0.9.2", + "rand 0.9.4", "rand_distr 0.5.1", "statrs", ] @@ -214,13 +214,6 @@ dependencies = [ "pyo3", ] -[[package]] -name = "ci_r" -version = "0.1.0" -dependencies = [ - "ci_core", -] - [[package]] name = "ciborium" version = "0.2.2" @@ -248,11 +241,20 @@ dependencies = [ "half", ] +[[package]] +name = "cir" +version = "0.1.0" +dependencies = [ + "anyhow", + "ci_core", + "extendr-api", +] + [[package]] name = "clap" -version = "4.6.0" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" dependencies = [ "clap_builder", ] @@ -545,6 +547,38 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "extendr-api" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "803569de0d273b4bf281871046a7d63a23cc12776bdb5b63de5c1e81aae30728" +dependencies = [ + "extendr-ffi", + "extendr-macros", + "ndarray", + "once_cell", + "paste", + "readonly", +] + +[[package]] +name = "extendr-ffi" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ba82ddd48e85202654997b81e4b1d39c0c54b5dcd7cae92705f807bf528efcf" + +[[package]] +name = "extendr-macros" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba8fad8d2a0d0651b1947042cf3a8beddc73d39cec3485b200fdfd24cb3bb6aa" +dependencies = [ + "lazy_static", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "fastrand" version = "2.4.1" @@ -694,9 +728,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.16.1" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" [[package]] name = "heck" @@ -867,12 +901,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.13.1" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45a8a2b9cb3e0b0c1803dbb0758ffac5de2f425b23c28f518faabd9d805342ff" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.16.1", + "hashbrown 0.17.0", "serde", "serde_core", ] @@ -927,9 +961,9 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "js-sys" -version = "0.3.94" +version = "0.3.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e04e2ef80ce82e13552136fabeef8a5ed1f985a96805761cbb9a2c34e7664d9" +checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca" dependencies = [ "once_cell", "wasm-bindgen", @@ -948,18 +982,18 @@ dependencies = [ [[package]] name = "lapack-sys" -version = "0.15.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "314b879030845b68571809a6978e52d3b67ac5fba07e77b1b317b484092e2fb5" +checksum = "447f56c85fb410a7a3d36701b2153c1018b1d2b908c5fbaf01c1b04fac33bcbe" dependencies = [ "libc", ] [[package]] name = "lax" -version = "0.18.0" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda593cb87a3b1c06625e73710812005caf1523e45b0b898adcd7716602f8ba2" +checksum = "1048f58cdc36e726e1c5ef81ec7ac9b1be2fce6b705786514b5a4f8c63487867" dependencies = [ "cauchy", "intel-mkl-src", @@ -984,9 +1018,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.184" +version = "0.2.185" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af" +checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f" [[package]] name = "libm" @@ -996,9 +1030,9 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libredox" -version = "0.1.15" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ddbf48fd451246b1f8c2610bd3b4ac0cc6e149d89832867093ab69a17194f08" +checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" dependencies = [ "bitflags", "libc", @@ -1061,7 +1095,7 @@ dependencies = [ "num-complex", "num-rational", "num-traits", - "rand 0.8.5", + "rand 0.8.6", "rand_distr 0.4.3", "simba", "typenum", @@ -1086,9 +1120,9 @@ dependencies = [ [[package]] name = "ndarray" -version = "0.17.2" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "520080814a7a6b4a6e9070823bb24b4531daac8c4627e08ba5de8c5ef2f2752d" +checksum = "882ed72dce9365842bf196bdeedf5055305f11fc8c03dee7bb0194a6cad34841" dependencies = [ "approx", "cblas-sys", @@ -1104,9 +1138,9 @@ dependencies = [ [[package]] name = "ndarray-linalg" -version = "0.18.1" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb0783188ff249ab498417e0477f7fade3b312d1d287314ca76de570d9a83ee0" +checksum = "820b7fdcc3f1fd48c31f4c85bdefe93ef2d0e8b69ff955c4d499ea6db2f19d44" dependencies = [ "cauchy", "katexit", @@ -1114,7 +1148,7 @@ dependencies = [ "ndarray", "num-complex", "num-traits", - "rand 0.8.5", + "rand 0.8.6", "thiserror 2.0.18", ] @@ -1135,7 +1169,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" dependencies = [ "num-traits", - "rand 0.8.5", + "rand 0.8.6", "serde", ] @@ -1267,9 +1301,9 @@ dependencies = [ [[package]] name = "openssl" -version = "0.10.76" +version = "0.10.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "951c002c75e16ea2c65b8c7e4d3d51d5530d8dfa7d060b4776828c88cfb18ecf" +checksum = "f38c4372413cdaaf3cc79dd92d29d7d9f5ab09b51b10dded508fb90bb70b9222" dependencies = [ "bitflags", "cfg-if", @@ -1299,9 +1333,9 @@ checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] name = "openssl-sys" -version = "0.9.112" +version = "0.9.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57d55af3b3e226502be1526dfdba67ab0e9c96fc293004e79576b2b9edb0dbdb" +checksum = "13ce1245cd07fcc4cfdb438f7507b0c7e4f3849a69fd84d52374c66d83741bb6" dependencies = [ "cc", "libc", @@ -1347,9 +1381,9 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pkg-config" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" [[package]] name = "plain" @@ -1393,9 +1427,9 @@ checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "portable-atomic-util" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "091397be61a01d4be58e7841595bd4bfedb15f1cd54977d79b8271e94ed799a3" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" dependencies = [ "portable-atomic", ] @@ -1469,7 +1503,7 @@ dependencies = [ "bit-vec", "bitflags", "num-traits", - "rand 0.9.2", + "rand 0.9.4", "rand_chacha 0.9.0", "rand_xorshift", "regex-syntax", @@ -1565,9 +1599,9 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "rand" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" dependencies = [ "libc", "rand_chacha 0.3.1", @@ -1576,9 +1610,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.2" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.5", @@ -1629,7 +1663,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31" dependencies = [ "num-traits", - "rand 0.8.5", + "rand 0.8.6", ] [[package]] @@ -1639,7 +1673,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8615d50dcf34fa31f7ab52692afec947c4dd0ab803cc87cb3b0b4570ff7463" dependencies = [ "num-traits", - "rand 0.9.2", + "rand 0.9.4", ] [[package]] @@ -1659,9 +1693,9 @@ checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" [[package]] name = "rayon" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" dependencies = [ "either", "rayon-core", @@ -1677,11 +1711,22 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "readonly" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2a62d85ed81ca5305dc544bd42c8804c5060b78ffa5ad3c64b0fb6a8c13d062" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "redox_syscall" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" +checksum = "f450ad9c3b1da563fb6948a8e0fb0fb9269711c9c73d9ea1de5058c79c8d643a" dependencies = [ "bitflags", ] @@ -1772,9 +1817,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.37" +version = "0.23.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +checksum = "69f9466fb2c14ea04357e91413efb882e2a6d4a406e625449bc0a5d360d53a21" dependencies = [ "log", "once_cell", @@ -1796,9 +1841,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.10" +version = "0.103.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" dependencies = [ "ring", "rustls-pki-types", @@ -1988,7 +2033,7 @@ dependencies = [ "approx", "nalgebra", "num-traits", - "rand 0.8.5", + "rand 0.8.6", ] [[package]] @@ -2177,9 +2222,9 @@ checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" [[package]] name = "typenum" -version = "1.19.0" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" [[package]] name = "unarray" @@ -2278,9 +2323,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" -version = "1.23.0" +version = "1.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9" +checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" dependencies = [ "getrandom 0.4.2", "js-sys", @@ -2326,11 +2371,11 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.2+wasi-0.2.9" +version = "1.0.3+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.57.1", ] [[package]] @@ -2339,14 +2384,14 @@ version = "0.4.0+wasi-0.3.0-rc-2026-01-06" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.51.0", ] [[package]] name = "wasm-bindgen" -version = "0.2.117" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0551fc1bb415591e3372d0bc4780db7e587d84e2a7e79da121051c5c4b89d0b0" +checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" dependencies = [ "cfg-if", "once_cell", @@ -2357,9 +2402,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.117" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fbdf9a35adf44786aecd5ff89b4563a90325f9da0923236f6104e603c7e86be" +checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2367,9 +2412,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.117" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dca9693ef2bab6d4e6707234500350d8dad079eb508dca05530c85dc3a529ff2" +checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" dependencies = [ "bumpalo", "proc-macro2", @@ -2380,9 +2425,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.117" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39129a682a6d2d841b6c429d0c51e5cb0ed1a03829d8b3d1e69a011e62cb3d3b" +checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" dependencies = [ "unicode-ident", ] @@ -2423,9 +2468,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.94" +version = "0.3.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd70027e39b12f0849461e08ffc50b9cd7688d942c1c8e3c7b22273236b4dd0a" +checksum = "4f2dfbb17949fa2088e5d39408c48368947b86f7834484e87b73de55bc14d97d" dependencies = [ "js-sys", "wasm-bindgen", @@ -2433,9 +2478,9 @@ dependencies = [ [[package]] name = "webpki-root-certs" -version = "1.0.6" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca" +checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c" dependencies = [ "rustls-pki-types", ] @@ -2446,14 +2491,14 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" dependencies = [ - "webpki-roots 1.0.6", + "webpki-roots 1.0.7", ] [[package]] name = "webpki-roots" -version = "1.0.6" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" +checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" dependencies = [ "rustls-pki-types", ] @@ -2702,6 +2747,12 @@ dependencies = [ "wit-bindgen-rust-macro", ] +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + [[package]] name = "wit-bindgen-core" version = "0.51.0" diff --git a/Cargo.toml b/Cargo.toml index 1b56066..8e28563 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,14 +2,14 @@ members = [ "crates/ci-core", "crates/ci-python", - "crates/ci-r", + "crates/cir/src/rust", "crates/ci-js", ] resolver = "2" [workspace.dependencies] anyhow = "1" -ndarray = "0.17" +ndarray = "0.16" statrs = { version = "0.18.0"} diff --git a/crates/ci-core/Cargo.toml b/crates/ci-core/Cargo.toml index bd94090..07339aa 100644 --- a/crates/ci-core/Cargo.toml +++ b/crates/ci-core/Cargo.toml @@ -20,13 +20,13 @@ rand = "0.9" libm = "0.2.16" [target.'cfg(target_os = "linux")'.dependencies] -ndarray-linalg = { version = "0.18.1", features = ["openblas-system"] } +ndarray-linalg = { version = "0.17", features = ["openblas-system"] } [target.'cfg(target_os = "macos")'.dependencies] -ndarray-linalg = { version = "0.18.1", features = ["openblas-system"] } +ndarray-linalg = { version = "0.17", features = ["openblas-system"] } [target.'cfg(windows)'.dependencies] -ndarray-linalg = { version = "0.18.1", features = ["intel-mkl-static"] } +ndarray-linalg = { version = "0.17", features = ["intel-mkl-static"] } [dev-dependencies] criterion = { workspace = true } diff --git a/crates/ci-core/src/ci_tests/chi_squared.rs b/crates/ci-core/src/ci_tests/chi_squared.rs index d32ec3c..9106d7d 100644 --- a/crates/ci-core/src/ci_tests/chi_squared.rs +++ b/crates/ci-core/src/ci_tests/chi_squared.rs @@ -10,6 +10,7 @@ pub struct ChiSquared { } impl ChiSquared { + #[must_use] pub fn new(boolean: bool, significance_level: f64) -> Self { Self { boolean, diff --git a/crates/ci-core/src/ci_tests/cressie_read.rs b/crates/ci-core/src/ci_tests/cressie_read.rs index 9ce61f8..47ffb16 100644 --- a/crates/ci-core/src/ci_tests/cressie_read.rs +++ b/crates/ci-core/src/ci_tests/cressie_read.rs @@ -10,6 +10,7 @@ pub struct CressieRead { } impl CressieRead { + #[must_use] pub fn new(boolean: bool, significance_level: f64) -> Self { Self { boolean, diff --git a/crates/ci-core/src/ci_tests/freeman_tukey.rs b/crates/ci-core/src/ci_tests/freeman_tukey.rs index b4474bc..27c018c 100644 --- a/crates/ci-core/src/ci_tests/freeman_tukey.rs +++ b/crates/ci-core/src/ci_tests/freeman_tukey.rs @@ -10,6 +10,7 @@ pub struct FreemanTukey { } impl FreemanTukey { + #[must_use] pub fn new(boolean: bool, significance_level: f64) -> Self { Self { boolean, diff --git a/crates/ci-core/src/ci_tests/log_likelihood.rs b/crates/ci-core/src/ci_tests/log_likelihood.rs index 0d443b2..296437f 100644 --- a/crates/ci-core/src/ci_tests/log_likelihood.rs +++ b/crates/ci-core/src/ci_tests/log_likelihood.rs @@ -10,6 +10,7 @@ pub struct LogLikelihood { } impl LogLikelihood { + #[must_use] pub fn new(boolean: bool, significance_level: f64) -> Self { Self { boolean, diff --git a/crates/ci-core/src/ci_tests/mod.rs b/crates/ci-core/src/ci_tests/mod.rs index e7efdd0..0f53b52 100644 --- a/crates/ci-core/src/ci_tests/mod.rs +++ b/crates/ci-core/src/ci_tests/mod.rs @@ -1,10 +1,10 @@ -mod chi_squared; -mod cressie_read; -mod freeman_tukey; -mod log_likelihood; -mod modified_likelihood; -mod pearson_correlation; -mod pearson_equivalence; +pub mod chi_squared; +pub mod cressie_read; +pub mod freeman_tukey; +pub mod log_likelihood; +pub mod modified_likelihood; +pub mod pearson_correlation; +pub mod pearson_equivalence; use chi_squared::ChiSquared; use cressie_read::CressieRead; @@ -16,6 +16,9 @@ use pearson_equivalence::PearsonEquivalence; use crate::registry::Registry; +/// # Panics +/// +/// Panics if any test name is already registered (indicates a duplicate registration bug). pub fn register_all_tests(registry: &mut Registry) { registry .add_to_registry("chi_square", ChiSquared::new(true, 0.05)) diff --git a/crates/ci-core/src/ci_tests/modified_likelihood.rs b/crates/ci-core/src/ci_tests/modified_likelihood.rs index ed97967..4ca950e 100644 --- a/crates/ci-core/src/ci_tests/modified_likelihood.rs +++ b/crates/ci-core/src/ci_tests/modified_likelihood.rs @@ -10,6 +10,7 @@ pub struct ModifiedLikelihood { } impl ModifiedLikelihood { + #[must_use] pub fn new(boolean: bool, significance_level: f64) -> Self { Self { boolean, diff --git a/crates/ci-core/src/ci_tests/pearson_correlation.rs b/crates/ci-core/src/ci_tests/pearson_correlation.rs index 34b8576..1214b7a 100644 --- a/crates/ci-core/src/ci_tests/pearson_correlation.rs +++ b/crates/ci-core/src/ci_tests/pearson_correlation.rs @@ -21,6 +21,7 @@ pub struct PearsonCorrelation { } impl PearsonCorrelation { + #[must_use] pub fn new(boolean: bool, significance_level: f64) -> Self { Self { boolean, @@ -84,6 +85,7 @@ impl CITest for PearsonCorrelation { } /// Construct the appropriate [`TestResult`] variant based on the `boolean` flag. +#[must_use] pub fn wrap_result( boolean: bool, p_value: f64, diff --git a/crates/ci-core/src/ci_tests/pearson_equivalence.rs b/crates/ci-core/src/ci_tests/pearson_equivalence.rs index b3010c6..4b44b11 100644 --- a/crates/ci-core/src/ci_tests/pearson_equivalence.rs +++ b/crates/ci-core/src/ci_tests/pearson_equivalence.rs @@ -11,6 +11,7 @@ pub struct PearsonEquivalence { } impl PearsonEquivalence { + #[must_use] pub fn new(boolean: bool, significance_level: f64, delta_threshold: f64) -> Self { Self { boolean, @@ -87,6 +88,7 @@ impl CITest for PearsonEquivalence { } } +#[must_use] pub fn wrap_result( boolean: bool, p_value: f64, diff --git a/crates/ci-core/src/lib.rs b/crates/ci-core/src/lib.rs index 3294e97..abc87ef 100644 --- a/crates/ci-core/src/lib.rs +++ b/crates/ci-core/src/lib.rs @@ -1,4 +1,4 @@ -mod ci_tests; +pub mod ci_tests; pub mod registry; pub mod strategy; pub mod utils; diff --git a/crates/ci-r/Cargo.toml b/crates/ci-r/Cargo.toml deleted file mode 100644 index 13b8caf..0000000 --- a/crates/ci-r/Cargo.toml +++ /dev/null @@ -1,16 +0,0 @@ -[package] -name = "ci_r" -version = "0.1.0" -edition = "2021" -authors = ["Your Team "] -license = "" -description = "extendr bindings for Conditional Independence Testing" -repository = "" - -[dependencies] -ci_core = { path = "../ci-core" } - -[lints] -workspace = true - -[lib] diff --git a/crates/ci-r/README.md b/crates/ci-r/README.md deleted file mode 100644 index 41e4a99..0000000 --- a/crates/ci-r/README.md +++ /dev/null @@ -1 +0,0 @@ -extendr bindings \ No newline at end of file diff --git a/crates/ci-r/src/lib.rs b/crates/ci-r/src/lib.rs deleted file mode 100644 index 8b13789..0000000 --- a/crates/ci-r/src/lib.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/crates/cir/.Rbuildignore b/crates/cir/.Rbuildignore new file mode 100644 index 0000000..a020d1a --- /dev/null +++ b/crates/cir/.Rbuildignore @@ -0,0 +1,6 @@ +^\.vscode$ +^src/\.cargo$ +^src/rust/vendor$ +^src/rust/target$ +^src/Makevars$ +^src/Makevars\.win$ diff --git a/crates/cir/.gitignore b/crates/cir/.gitignore new file mode 100644 index 0000000..20cff42 --- /dev/null +++ b/crates/cir/.gitignore @@ -0,0 +1,3 @@ +src/rust/vendor +src/Makevars +src/Makevars.win diff --git a/crates/cir/.vscode/settings.json b/crates/cir/.vscode/settings.json new file mode 100644 index 0000000..a0fd38b --- /dev/null +++ b/crates/cir/.vscode/settings.json @@ -0,0 +1,15 @@ +{ + "rust-analyzer.linkedProjects": [ + "${workspaceFolder}/src/rust/Cargo.toml" + ], + "files.associations": { + "Makevars": "makefile", + "Makevars.in": "makefile", + "Makevars.win": "makefile", + "Makevars.win.in": "makefile", + "configure": "shellscript", + "configure.win": "shellscript", + "cleanup": "shellscript", + "cleanup.win": "shellscript" + } +} diff --git a/crates/cir/DESCRIPTION b/crates/cir/DESCRIPTION new file mode 100644 index 0000000..3f20518 --- /dev/null +++ b/crates/cir/DESCRIPTION @@ -0,0 +1,22 @@ +Package: cir +Title: Conditional Independence Testing +Version: 0.0.0.9000 +Authors@R: + person("GIP", "House", email = "giphouse@example.com", role = c("aut", "cre")) +Description: R bindings for a collection of conditional independence tests + implemented in Rust. Supports discrete, continuous, and mixed data via + chi-squared, log-likelihood, Cressie-Read, Freeman-Tukey, Pearson + correlation, modified log-likelihood, and Pearson equivalence tests. +License: MIT +Encoding: UTF-8 +Roxygen: list(markdown = TRUE) +RoxygenNote: 7.3.3 +Config/rextendr/version: 0.5.0 +SystemRequirements: Cargo (Rust's package manager), rustc >= 1.65.0, xz +Depends: + R (>= 4.2) +Suggests: + devtools, + rextendr, + testthat (>= 3.0.0) +Config/testthat/edition: 3 diff --git a/crates/cir/NAMESPACE b/crates/cir/NAMESPACE new file mode 100644 index 0000000..5d83515 --- /dev/null +++ b/crates/cir/NAMESPACE @@ -0,0 +1,3 @@ +# Generated by roxygen2: do not edit by hand + +useDynLib(cir, .registration = TRUE) diff --git a/crates/cir/R/extendr-wrappers.R b/crates/cir/R/extendr-wrappers.R new file mode 100644 index 0000000..02e79f9 --- /dev/null +++ b/crates/cir/R/extendr-wrappers.R @@ -0,0 +1,31 @@ +# Generated by extendr: Do not edit by hand +# nolint start + +#' @usage NULL +#' @useDynLib cir, .registration = TRUE +NULL + +#' Returns a sorted vector of all registered CI test names. +list_ci_tests <- function() .Call(wrap__list_ci_tests) + +#' Returns a sorted vector of CI test names compatible with the given data type. +#' +#' `data_type` must be one of `"discrete"`, `"continuous"`, or `"mixed"` (case-insensitive). +#' Returns an error for any other value. +list_ci_tests_for <- function(data_type) .Call(wrap__list_ci_tests_for, data_type) + +chi_squared_test <- function(x_values, y_values, z, boolean, significance_level) .Call(wrap__chi_squared_test, x_values, y_values, z, boolean, significance_level) + +log_likelihood_test <- function(x_values, y_values, z, boolean, significance_level) .Call(wrap__log_likelihood_test, x_values, y_values, z, boolean, significance_level) + +cressie_read_test <- function(x_values, y_values, z, boolean, significance_level) .Call(wrap__cressie_read_test, x_values, y_values, z, boolean, significance_level) + +pearson_correlation_test <- function(x_values, y_values, z, boolean, significance_level) .Call(wrap__pearson_correlation_test, x_values, y_values, z, boolean, significance_level) + +freeman_tukey_test <- function(x_values, y_values, z, boolean, significance_level) .Call(wrap__freeman_tukey_test, x_values, y_values, z, boolean, significance_level) + +modified_likelihood_test <- function(x_values, y_values, z, boolean, significance_level) .Call(wrap__modified_likelihood_test, x_values, y_values, z, boolean, significance_level) + +pearson_equivalence_test <- function(x_values, y_values, z, boolean, significance_level) .Call(wrap__pearson_equivalence_test, x_values, y_values, z, boolean, significance_level) + +# nolint end diff --git a/crates/cir/README.md b/crates/cir/README.md new file mode 100644 index 0000000..b49d77a --- /dev/null +++ b/crates/cir/README.md @@ -0,0 +1,35 @@ +extendr bindings + +## Usage + +Install R, then open an R session: + +```sh +R +``` + +Install `pak` (the modern R package manager that handles system dependencies automatically): + +```r +install.packages("pak", repos = "https://cloud.r-project.org") +``` + +Install dev dependencies: + +```r +pak::pak(c("devtools", "rextendr")) +``` + +### Regenerate bindings + +```r +setwd("crates/cir") +rextendr::document() # regenerates R wrappers and compiles Rust +``` + +### Load and test + +```r +devtools::load_all() # compiles Rust + loads the package +devtools::test() # run all tests +``` diff --git a/crates/cir/cleanup b/crates/cir/cleanup new file mode 100644 index 0000000..e346d71 --- /dev/null +++ b/crates/cir/cleanup @@ -0,0 +1 @@ +rm -f src/Makevars diff --git a/crates/cir/cleanup.win b/crates/cir/cleanup.win new file mode 100644 index 0000000..a182174 --- /dev/null +++ b/crates/cir/cleanup.win @@ -0,0 +1 @@ +rm -f src/Makevars.win diff --git a/crates/cir/configure b/crates/cir/configure new file mode 100755 index 0000000..c608b11 --- /dev/null +++ b/crates/cir/configure @@ -0,0 +1,3 @@ +#!/usr/bin/env sh +: "${R_HOME=`R RHOME`}" +"${R_HOME}/bin/Rscript" tools/config.R diff --git a/crates/cir/configure.win b/crates/cir/configure.win new file mode 100644 index 0000000..57eb255 --- /dev/null +++ b/crates/cir/configure.win @@ -0,0 +1,2 @@ +#!/usr/bin/env sh +"${R_HOME}/bin${R_ARCH_BIN}/Rscript.exe" tools/config.R diff --git a/crates/cir/src/.gitignore b/crates/cir/src/.gitignore new file mode 100644 index 0000000..24e51fc --- /dev/null +++ b/crates/cir/src/.gitignore @@ -0,0 +1,8 @@ +*.o +*.so +*.dll +target +.cargo +rust/vendor +Makevars +Makevars.win diff --git a/crates/cir/src/Makevars.in b/crates/cir/src/Makevars.in new file mode 100644 index 0000000..a687d41 --- /dev/null +++ b/crates/cir/src/Makevars.in @@ -0,0 +1,52 @@ +TARGET_DIR = ./rust/target +LIBDIR = $(TARGET_DIR)/@LIBDIR@ +STATLIB = $(LIBDIR)/libcir.a +PKG_LIBS = -L$(LIBDIR) -lcir + +all: $(SHLIB) rust_clean + +.PHONY: $(STATLIB) + +$(SHLIB): $(STATLIB) + +CARGOTMP = $(CURDIR)/.cargo +VENDOR_DIR = $(CURDIR)/vendor + + +# RUSTFLAGS appends --print=native-static-libs to ensure that +# the correct linkers are used. Use this for debugging if need. +# +# CRAN note: Cargo and Rustc versions are reported during +# configure via tools/msrv.R. +# +# If a vendor directory exists, it is used for offline compilation. Otherwise if +# vendor.tar.xz exists, it is unzipped and used for offline compilation. +$(STATLIB): + + if [ -d ./vendor ]; then \ + echo "=== Using offline vendor directory ==="; \ + mkdir -p $(CARGOTMP) && \ + cp rust/vendor-config.toml $(CARGOTMP)/config.toml; \ + elif [ -f ./rust/vendor.tar.xz ]; then \ + echo "=== Using offline vendor tarball ==="; \ + tar xf rust/vendor.tar.xz && \ + mkdir -p $(CARGOTMP) && \ + cp rust/vendor-config.toml $(CARGOTMP)/config.toml; \ + fi + + export CARGO_HOME=$(CARGOTMP) && \ + export PATH="$(PATH):$(HOME)/.cargo/bin" && \ + @PANIC_EXPORTS@RUSTFLAGS="$(RUSTFLAGS) --print=native-static-libs" cargo build @CRAN_FLAGS@ --lib @PROFILE@ --manifest-path=./rust/Cargo.toml --target-dir $(TARGET_DIR) @TARGET@ + + export CARGO_HOME=$(CARGOTMP) && \ + export PATH="$(PATH):$(HOME)/.cargo/bin" && \ + cargo run @CRAN_FLAGS@ --bin document --manifest-path=./rust/Cargo.toml --target-dir $(TARGET_DIR) @TARGET@ + + # Always clean up CARGOTMP + rm -Rf $(CARGOTMP); + +rust_clean: $(SHLIB) + rm -Rf $(CARGOTMP) $(VENDOR_DIR) @CLEAN_TARGET@ + +clean: + rm -Rf $(SHLIB) $(STATLIB) $(OBJECTS) $(TARGET_DIR) $(VENDOR_DIR) diff --git a/crates/cir/src/Makevars.win.in b/crates/cir/src/Makevars.win.in new file mode 100644 index 0000000..6f72063 --- /dev/null +++ b/crates/cir/src/Makevars.win.in @@ -0,0 +1,51 @@ +TARGET = $(subst 64,x86_64,$(subst 32,i686,$(WIN)))-pc-windows-gnu + +TARGET_DIR = ./rust/target +LIBDIR = $(TARGET_DIR)/$(TARGET)/@LIBDIR@ +STATLIB = $(LIBDIR)/libcir.a +PKG_LIBS = -L$(LIBDIR) -lcir -lws2_32 -ladvapi32 -luserenv -lbcrypt -lntdll + +all: $(SHLIB) rust_clean + +.PHONY: $(STATLIB) + +$(SHLIB): $(STATLIB) + +CARGOTMP = $(CURDIR)/.cargo +VENDOR_DIR = vendor + +$(STATLIB): + mkdir -p $(TARGET_DIR)/libgcc_mock + touch $(TARGET_DIR)/libgcc_mock/libgcc_eh.a + + # If a vendor directory exists, it is used for offline compilation. Otherwise if + # vendor.tar.xz exists, it is unzipped and used for offline compilation. + if [ -d ./vendor ]; then \ + echo "=== Using offline vendor directory ==="; \ + mkdir -p $(CARGOTMP) && \ + cp rust/vendor-config.toml $(CARGOTMP)/config.toml; \ + elif [ -f ./rust/vendor.tar.xz ]; then \ + echo "=== Using offline vendor tarball ==="; \ + tar xf rust/vendor.tar.xz && \ + mkdir -p $(CARGOTMP) && \ + cp rust/vendor-config.toml $(CARGOTMP)/config.toml; \ + fi + + # Build the project using Cargo with additional flags + export CARGO_HOME=$(CARGOTMP) && \ + export LIBRARY_PATH="$(LIBRARY_PATH);$(CURDIR)/$(TARGET_DIR)/libgcc_mock" && \ + RUSTFLAGS="$(RUSTFLAGS) --print=native-static-libs" cargo build @CRAN_FLAGS@ --target=$(TARGET) --lib @PROFILE@ --manifest-path=rust/Cargo.toml --target-dir=$(TARGET_DIR) + + # Generate wrappers + export CARGO_HOME=$(CARGOTMP) && \ + export PATH="$(PATH):$(HOME)/.cargo/bin" && \ + cargo run @CRAN_FLAGS@ --bin document --target $(TARGET) --manifest-path=./rust/Cargo.toml --target-dir $(TARGET_DIR) + + # Always clean up CARGOTMP + rm -Rf $(CARGOTMP); + +rust_clean: $(SHLIB) + rm -Rf $(CARGOTMP) $(VENDOR_DIR) @CLEAN_TARGET@ + +clean: + rm -Rf $(SHLIB) $(STATLIB) $(OBJECTS) $(TARGET_DIR) $(VENDOR_DIR) diff --git a/crates/cir/src/cir-win.def b/crates/cir/src/cir-win.def new file mode 100644 index 0000000..6fe96d6 --- /dev/null +++ b/crates/cir/src/cir-win.def @@ -0,0 +1,2 @@ +EXPORTS +R_init_cir diff --git a/crates/cir/src/entrypoint.c b/crates/cir/src/entrypoint.c new file mode 100644 index 0000000..fc87ffb --- /dev/null +++ b/crates/cir/src/entrypoint.c @@ -0,0 +1,10 @@ +// We need to forward routine registration from C to Rust +// to avoid the linker removing the static library. + +void R_init_cir_extendr(void *dll); +void register_extendr_panic_hook(void); + +void R_init_cir(void *dll) { + register_extendr_panic_hook(); + R_init_cir_extendr(dll); +} diff --git a/crates/cir/src/rust/Cargo.toml b/crates/cir/src/rust/Cargo.toml new file mode 100644 index 0000000..f0b3dbb --- /dev/null +++ b/crates/cir/src/rust/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = 'cir' +publish = false +version = '0.1.0' +edition = '2021' +rust-version = '1.65' + +[lib] +crate-type = [ 'rlib', 'staticlib' ] +name = 'cir' + +[[bin]] +name = 'document' +path = 'document.rs' + +[dependencies] +ci_core = { path = "../../../ci-core" } +anyhow = "1" +extendr-api = { version = '0.9.0', features = ['ndarray'] } + +[profile.release] +lto = true +codegen-units = 1 diff --git a/crates/cir/src/rust/document.rs b/crates/cir/src/rust/document.rs new file mode 100644 index 0000000..ef5b73f --- /dev/null +++ b/crates/cir/src/rust/document.rs @@ -0,0 +1,20 @@ +// Generated by extendr: Do not edit by hand +fn main() -> Result<(), Box> { + let wrapper_path = "../R/extendr-wrappers.R"; + let header = "\ + # Generated by extendr: Do not edit by hand\n\ + # nolint start\n\ + \n\ + #' @usage NULL\n\ + #' @useDynLib cir, .registration = TRUE\n\ + NULL\n\ + \n\ + "; + let footer = "# nolint end\n"; + let wrappers = cir::get_cir_metadata() + .make_r_wrappers(true, "cir") + .map_err(|e| format!("failed to generate wrappers: {e}"))?; + std::fs::write(wrapper_path, format!("{header}{wrappers}{footer}")) + .map_err(|e| format!("failed to write {wrapper_path}: {e}"))?; + Ok(()) +} diff --git a/crates/cir/src/rust/src/lib.rs b/crates/cir/src/rust/src/lib.rs new file mode 100644 index 0000000..8cd84db --- /dev/null +++ b/crates/cir/src/rust/src/lib.rs @@ -0,0 +1,97 @@ +//! R bindings for the conditional independence testing library. +//! +//! Exposes CI test functions and registry queries to R via the `extendr` framework. +//! Each CI test accepts paired observation vectors and a conditioning matrix, returning +//! a named R list whose shape depends on whether the test runs in boolean or numeric mode. + +use ci_core::ci_tests::{ + chi_squared::ChiSquared, cressie_read::CressieRead, freeman_tukey::FreemanTukey, + log_likelihood::LogLikelihood, modified_likelihood::ModifiedLikelihood, + pearson_correlation::PearsonCorrelation, +}; +use ci_core::registry::Registry; +use ci_core::strategy::{CITest, CITestDataType}; +use extendr_api::prelude::*; +use ndarray::{ArrayView1, ArrayView2}; +mod util; + +/// Generates an R-callable wrapper for a [`CITest`] implementation. +/// +/// The generated function signature is: +/// ```text +/// fn $fn_name(x_values, y_values, z, boolean, significance_level) -> Robj +/// ``` +/// - `x_values` / `y_values`: paired observation vectors. +/// - `z`: conditioning matrix; pass a 0-column matrix for unconditional tests. +/// - `boolean`: when `true`, returns only an independence verdict at `significance_level` +/// instead of the raw test statistic and p-value. +/// - `significance_level`: threshold used only when `boolean` is `true`. +macro_rules! r_ci_test { + ($fn_name:ident, $inner:ty) => { + #[extendr] + fn $fn_name( + x_values: ArrayView1, + y_values: ArrayView1, + z: ArrayView2, + boolean: bool, + significance_level: f64, + ) -> anyhow::Result { + let citest = <$inner>::new(boolean, significance_level); + let result = citest.run_test(x_values.to_owned(), y_values.to_owned(), z.to_owned())?; + Ok(util::test_result_to_robj(result)) + } + }; +} + +/// Returns a sorted vector of all registered CI test names. +#[extendr] +fn list_ci_tests() -> anyhow::Result> { + let registry = Registry::new(); + let mut tests: Vec = registry.all_tests()?.map(String::from).collect(); + tests.sort(); + Ok(tests) +} + +/// Returns a sorted vector of CI test names compatible with the given data type. +/// +/// `data_type` must be one of `"discrete"`, `"continuous"`, or `"mixed"` (case-insensitive). +/// Returns an error for any other value. +#[extendr] +fn list_ci_tests_for(data_type: &str) -> anyhow::Result> { + let dt = match data_type.to_lowercase().as_str() { + "discrete" => CITestDataType::Discrete, + "continuous" => CITestDataType::Continuous, + "mixed" => CITestDataType::Mixed, + _ => anyhow::bail!( + "Unknown data type: '{data_type}'. Use 'discrete', 'continuous', or 'mixed'." + ), + }; + let registry = Registry::new(); + let mut tests: Vec = registry + .tests_with_data_type(&dt)? + .map(String::from) + .collect(); + tests.sort(); + Ok(tests) +} + +r_ci_test!(chi_squared_test, ChiSquared); +r_ci_test!(log_likelihood_test, LogLikelihood); +r_ci_test!(cressie_read_test, CressieRead); +r_ci_test!(pearson_correlation_test, PearsonCorrelation); +r_ci_test!(freeman_tukey_test, FreemanTukey); +r_ci_test!(modified_likelihood_test, ModifiedLikelihood); +//r_ci_test!(pearson_equivalence_test, PearsonEquivalence); + +extendr_module! { + mod cir; + fn list_ci_tests; + fn list_ci_tests_for; + fn chi_squared_test; + fn log_likelihood_test; + fn cressie_read_test; + fn pearson_correlation_test; + fn freeman_tukey_test; + fn modified_likelihood_test; + //fn pearson_equivalence_test; +} diff --git a/crates/cir/src/rust/src/util.rs b/crates/cir/src/rust/src/util.rs new file mode 100644 index 0000000..4804aaa --- /dev/null +++ b/crates/cir/src/rust/src/util.rs @@ -0,0 +1,26 @@ +//! Conversion utilities from core result types to R objects. + +use ci_core::strategy::TestResult; +use extendr_api::prelude::*; + +/// Converts a [`TestResult`] into a named R list. +/// +/// The returned list always contains a `kind` field identifying the variant: +/// - `"pvalue"` — also contains `p_value` and `coefficient`. +/// - `"statistic"` — also contains `statistic`, `p_value`, and `df` (degrees of freedom). +/// - `"boolean"` — also contains `independent` (logical). +pub fn test_result_to_robj(r: TestResult) -> Robj { + match r { + TestResult::PValue(p, coef) => { + list!(kind = "pvalue", p_value = p, coefficient = coef,).into() + } + TestResult::Statistic(p, stat, df) => list!( + kind = "statistic", + statistic = stat, + p_value = p, + df = df as i32, + ) + .into(), + TestResult::Boolean(b) => list!(kind = "boolean", independent = b,).into(), + } +} diff --git a/crates/cir/tests/testthat/test-chi_squared.R b/crates/cir/tests/testthat/test-chi_squared.R new file mode 100644 index 0000000..3672e30 --- /dev/null +++ b/crates/cir/tests/testthat/test-chi_squared.R @@ -0,0 +1,45 @@ +library(cir) + +test_that("independent data is not rejected", { + x = c(1.0, 1.0, 2.0, 2.0, 1.0, 1.0, 2.0, 2.0) + y = c(1.0, 2.0, 1.0, 2.0, 1.0, 2.0, 1.0, 2.0) + z = matrix(0, nrow = 8, ncol = 0) + + result = chi_squared_test(x, y, z, FALSE, 0.05) + expect_equal(result$kind, "statistic") + expect_true(result$statistic < 1e-9) + expect_true(result$p_value >= 0.99) + expect_equal(result$df, 1) + }) + +test_that("dependent data is rejected", { + x = c(1., 1., 1., 1., 2., 2., 2., 2.) + y = c(1., 1., 1., 1., 2., 2., 2., 2.) + z = matrix(0, nrow = 8, ncol = 0) + + result = chi_squared_test(x, y, z, FALSE, 0.05) + expect_equal(result$kind, "statistic") + expect_true(abs(result$statistic - 8.0) < 1e-9) + expect_true(abs(result$p_value - 0.004677734981047276) < 1e-12) + expect_equal(result$df, 1) + }) + +test_that("boolean mode returns independent=TRUE for independent data", { + x = c(1.0, 1.0, 2.0, 2.0, 1.0, 1.0, 2.0, 2.0) + y = c(1.0, 2.0, 1.0, 2.0, 1.0, 2.0, 1.0, 2.0) + z = matrix(0, nrow = 8, ncol = 0) + + result = chi_squared_test(x, y, z, TRUE, 0.05) + expect_equal(result$kind, "boolean") + expect_true(result$independent) + }) + +test_that("boolean mode returns independent=FALSE for dependent data", { + x = c(1., 1., 1., 1., 2., 2., 2., 2.) + y = c(1., 1., 1., 1., 2., 2., 2., 2.) + z = matrix(0, nrow = 8, ncol = 0) + + result = chi_squared_test(x, y, z, TRUE, 0.05) + expect_equal(result$kind, "boolean") + expect_false(result$independent) + }) diff --git a/crates/cir/tests/testthat/test-cressie_read.R b/crates/cir/tests/testthat/test-cressie_read.R new file mode 100644 index 0000000..ad8d20c --- /dev/null +++ b/crates/cir/tests/testthat/test-cressie_read.R @@ -0,0 +1,78 @@ +library(cir) + +test_that("unconditional independent data is not rejected", { + x = c(1.0, 1.0, 2.0, 2.0, 1.0, 1.0, 2.0, 2.0) + y = c(1.0, 2.0, 1.0, 2.0, 1.0, 2.0, 1.0, 2.0) + z = matrix(0, nrow = 8, ncol = 0) + + result = cressie_read_test(x, y, z, FALSE, 0.05) + expect_equal(result$kind, "statistic") + expect_true(result$statistic < 1e-9) + expect_true(result$p_value > 0.99) + expect_equal(result$df, 1) + }) + +test_that("unconditional boolean accepts independent data", { + x = c(1.0, 1.0, 2.0, 2.0, 1.0, 1.0, 2.0, 2.0) + y = c(1.0, 2.0, 1.0, 2.0, 1.0, 2.0, 1.0, 2.0) + z = matrix(0, nrow = 8, ncol = 0) + + result = cressie_read_test(x, y, z, TRUE, 0.05) + expect_equal(result$kind, "boolean") + expect_true(result$independent) + }) + +test_that("unconditional dependent data is rejected", { + x = c(1., 1., 1., 1., 2., 2., 2., 2.) + y = c(1., 1., 1., 1., 2., 2., 2., 2.) + z = matrix(0, nrow = 8, ncol = 0) + + result = cressie_read_test(x, y, z, FALSE, 0.05) + expect_equal(result$kind, "statistic") + expect_true(result$statistic > 5.0) + expect_true(result$p_value < 0.05) + expect_equal(result$df, 1) + }) + +test_that("unconditional boolean rejects dependent data", { + x = c(1., 1., 1., 1., 2., 2., 2., 2.) + y = c(1., 1., 1., 1., 2., 2., 2., 2.) + z = matrix(0, nrow = 8, ncol = 0) + + result = cressie_read_test(x, y, z, TRUE, 0.05) + expect_equal(result$kind, "boolean") + expect_false(result$independent) + }) + +test_that("conditional independent data is not rejected", { + x = c(1.0, 1.0, 2.0, 2.0, 1.0, 1.0, 2.0, 2.0) + y = c(1.0, 2.0, 1.0, 2.0, 1.0, 2.0, 1.0, 2.0) + z = matrix(c(0., 0., 0., 0., 1., 1., 1., 1.), nrow = 8, ncol = 1) + + result = cressie_read_test(x, y, z, FALSE, 0.05) + expect_equal(result$kind, "statistic") + expect_true(result$statistic < 1e-9) + expect_true(result$p_value > 0.99) + expect_equal(result$df, 2) + }) + +test_that("conditional boolean accepts conditionally independent data", { + x = c(1.0, 1.0, 2.0, 2.0, 1.0, 1.0, 2.0, 2.0) + y = c(1.0, 2.0, 1.0, 2.0, 1.0, 2.0, 1.0, 2.0) + z = matrix(c(0., 0., 0., 0., 1., 1., 1., 1.), nrow = 8, ncol = 1) + + result = cressie_read_test(x, y, z, TRUE, 0.05) + expect_equal(result$kind, "boolean") + expect_true(result$independent) + }) + +test_that("conditional dependent data is rejected", { + x = c(1., 1., 2., 2., 1., 1., 2., 2.) + y = c(1., 1., 2., 2., 1., 1., 2., 2.) + z = matrix(c(0., 0., 0., 0., 1., 1., 1., 1.), nrow = 8, ncol = 1) + + result = cressie_read_test(x, y, z, FALSE, 0.05) + expect_equal(result$kind, "statistic") + expect_true(result$statistic > 5.0) + expect_true(result$p_value < 0.05) + }) diff --git a/crates/cir/tests/testthat/test-freeman_tukey.R b/crates/cir/tests/testthat/test-freeman_tukey.R new file mode 100644 index 0000000..b02b2d8 --- /dev/null +++ b/crates/cir/tests/testthat/test-freeman_tukey.R @@ -0,0 +1,35 @@ +library(cir) + +test_that("independent data is not rejected", { + x = c(1.0, 1.0, 2.0, 2.0, 1.0, 1.0, 2.0, 2.0) + y = c(1.0, 2.0, 1.0, 2.0, 1.0, 2.0, 1.0, 2.0) + z = matrix(0, nrow = 8, ncol = 0) + + result = freeman_tukey_test(x, y, z, FALSE, 0.05) + expect_equal(result$kind, "statistic") + expect_true(result$statistic < 1e-9) + expect_true(result$p_value >= 0.99) + expect_equal(result$df, 1) + }) + +test_that("dependent data is rejected", { + x = c(1., 1., 1., 1., 1., 1., 2., 2., 2., 2., 2., 2.) + y = c(1., 1., 1., 1., 1., 2., 1., 2., 2., 2., 2., 2.) + z = matrix(0, nrow = 12, ncol = 0) + + result = freeman_tukey_test(x, y, z, FALSE, 0.05) + expect_equal(result$kind, "statistic") + expect_true(abs(result$statistic - 6.319453539579289) < 1e-9) + expect_true(abs(result$p_value - 0.011942042564347121) < 1e-12) + expect_equal(result$df, 1) + }) + +test_that("boolean mode returns independent=TRUE for independent data", { + x = c(1.0, 1.0, 2.0, 2.0, 1.0, 1.0, 2.0, 2.0) + y = c(1.0, 2.0, 1.0, 2.0, 1.0, 2.0, 1.0, 2.0) + z = matrix(0, nrow = 8, ncol = 0) + + result = freeman_tukey_test(x, y, z, TRUE, 0.05) + expect_equal(result$kind, "boolean") + expect_true(result$independent) + }) diff --git a/crates/cir/tests/testthat/test-log_likelihood.R b/crates/cir/tests/testthat/test-log_likelihood.R new file mode 100644 index 0000000..58cad3d --- /dev/null +++ b/crates/cir/tests/testthat/test-log_likelihood.R @@ -0,0 +1,35 @@ +library(cir) + +test_that("independent data is not rejected", { + x = c(1.0, 1.0, 2.0, 2.0, 1.0, 1.0, 2.0, 2.0) + y = c(1.0, 2.0, 1.0, 2.0, 1.0, 2.0, 1.0, 2.0) + z = matrix(0, nrow = 8, ncol = 0) + + result = log_likelihood_test(x, y, z, FALSE, 0.05) + expect_equal(result$kind, "statistic") + expect_true(result$statistic < 1e-9) + expect_true(result$p_value >= 0.99) + expect_equal(result$df, 1) + }) + +test_that("dependent data is rejected", { + x = c(1., 1., 1., 1., 1., 1., 2., 2., 2., 2., 2., 2.) + y = c(1., 1., 1., 1., 1., 2., 1., 2., 2., 2., 2., 2.) + z = matrix(0, nrow = 12, ncol = 0) + + result = log_likelihood_test(x, y, z, FALSE, 0.05) + expect_equal(result$kind, "statistic") + expect_true(abs(result$statistic - 5.822063320647374) < 1e-9) + expect_true(abs(result$p_value - 0.015826368796540195) < 1e-12) + expect_equal(result$df, 1) + }) + +test_that("boolean mode returns independent=FALSE for dependent data", { + x = c(1., 1., 1., 1., 1., 1., 2., 2., 2., 2., 2., 2.) + y = c(1., 1., 1., 1., 1., 2., 1., 2., 2., 2., 2., 2.) + z = matrix(0, nrow = 12, ncol = 0) + + result = log_likelihood_test(x, y, z, TRUE, 0.05) + expect_equal(result$kind, "boolean") + expect_false(result$independent) + }) diff --git a/crates/cir/tests/testthat/test-modified_likelihood.R b/crates/cir/tests/testthat/test-modified_likelihood.R new file mode 100644 index 0000000..9204669 --- /dev/null +++ b/crates/cir/tests/testthat/test-modified_likelihood.R @@ -0,0 +1,35 @@ +library(cir) + +test_that("independent data is not rejected", { + x = c(1.0, 1.0, 2.0, 2.0, 1.0, 1.0, 2.0, 2.0) + y = c(1.0, 2.0, 1.0, 2.0, 1.0, 2.0, 1.0, 2.0) + z = matrix(0, nrow = 8, ncol = 0) + + result = modified_likelihood_test(x, y, z, FALSE, 0.05) + expect_equal(result$kind, "statistic") + expect_true(result$statistic < 1e-9) + expect_true(result$p_value >= 0.99) + expect_equal(result$df, 1) + }) + +test_that("dependent data is rejected", { + x = c(1., 1., 1., 1., 1., 1., 2., 2., 2., 2., 2., 2.) + y = c(1., 1., 1., 1., 1., 2., 1., 2., 2., 2., 2., 2.) + z = matrix(0, nrow = 12, ncol = 0) + + result = modified_likelihood_test(x, y, z, FALSE, 0.05) + expect_equal(result$kind, "statistic") + expect_true(abs(result$statistic - 7.053439978825427) < 1e-9) + expect_true(abs(result$p_value - 0.007911317670556329) < 1e-12) + expect_equal(result$df, 1) + }) + +test_that("boolean mode returns independent=FALSE for dependent data", { + x = c(1., 1., 1., 1., 1., 1., 2., 2., 2., 2., 2., 2.) + y = c(1., 1., 1., 1., 1., 2., 1., 2., 2., 2., 2., 2.) + z = matrix(0, nrow = 12, ncol = 0) + + result = modified_likelihood_test(x, y, z, TRUE, 0.05) + expect_equal(result$kind, "boolean") + expect_false(result$independent) + }) diff --git a/crates/cir/tests/testthat/test-pearson_correlation.R b/crates/cir/tests/testthat/test-pearson_correlation.R new file mode 100644 index 0000000..707f160 --- /dev/null +++ b/crates/cir/tests/testthat/test-pearson_correlation.R @@ -0,0 +1,134 @@ +library(cir) +N = 1000 + +test_that("unconditional independent data is not rejected", { + set.seed(42) + x = rnorm(N) + y = rnorm(N) + z = matrix(0, nrow = N, ncol = 0) + + result = pearson_correlation_test(x, y, z, FALSE, 0.05) + expect_equal(result$kind, "pvalue") + expect_true(result$p_value > 0.05) + expect_true(abs(result$coefficient) < 0.1) + }) + +test_that("unconditional boolean accepts independent data", { + set.seed(42) + x = rnorm(N) + y = rnorm(N) + z = matrix(0, nrow = N, ncol = 0) + + result = pearson_correlation_test(x, y, z, TRUE, 0.05) + expect_equal(result$kind, "boolean") + expect_true(result$independent) + }) + +test_that("unconditional dependent data is rejected", { + set.seed(42) + x = rnorm(N) + noise = rnorm(N, sd = 0.1) + y = 3 * x + noise + z = matrix(0, nrow = N, ncol = 0) + + result = pearson_correlation_test(x, y, z, FALSE, 0.05) + expect_equal(result$kind, "pvalue") + expect_true(result$p_value < 0.05) + expect_true(abs(result$coefficient) > 0.9) + }) + +test_that("unconditional boolean rejects dependent data", { + set.seed(42) + x = rnorm(N) + noise = rnorm(N, sd = 0.1) + y = 3 * x + noise + z = matrix(0, nrow = N, ncol = 0) + + result = pearson_correlation_test(x, y, z, TRUE, 0.05) + expect_equal(result$kind, "boolean") + expect_false(result$independent) + }) + +test_that("conditional independent data is not rejected", { + set.seed(42) + z_col = rnorm(N) + noise_x = rnorm(N, sd = 0.1) + noise_y = rnorm(N, sd = 0.1) + x = 3 * z_col + noise_x + y = 2 * z_col + noise_y + z = matrix(z_col, nrow = N, ncol = 1) + + result = pearson_correlation_test(x, y, z, FALSE, 0.05) + expect_equal(result$kind, "pvalue") + expect_true(result$p_value > 0.05) + expect_true(abs(result$coefficient) < 0.1) + }) + +test_that("conditional boolean accepts conditionally independent data", { + set.seed(42) + z_col = rnorm(N) + noise_x = rnorm(N, sd = 0.1) + noise_y = rnorm(N, sd = 0.1) + x = 3 * z_col + noise_x + y = 2 * z_col + noise_y + z = matrix(z_col, nrow = N, ncol = 1) + + result = pearson_correlation_test(x, y, z, TRUE, 0.05) + expect_equal(result$kind, "boolean") + expect_true(result$independent) + }) + +test_that("conditional dependent data (v-structure collider) is rejected", { + set.seed(42) + x = rnorm(N) + y = rnorm(N) + noise = rnorm(N, sd = 0.1) + z_col = 2 * x + 2 * y + noise + z = matrix(z_col, nrow = N, ncol = 1) + + result = pearson_correlation_test(x, y, z, FALSE, 0.05) + expect_equal(result$kind, "pvalue") + expect_true(result$p_value < 0.05) + expect_true(abs(result$coefficient) > 0.9) + }) + +test_that("conditional boolean rejects dependent data (v-structure collider)", { + set.seed(42) + x = rnorm(N) + y = rnorm(N) + noise = rnorm(N, sd = 0.1) + z_col = 2 * x + 2 * y + noise + z = matrix(z_col, nrow = N, ncol = 1) + + result = pearson_correlation_test(x, y, z, TRUE, 0.05) + expect_equal(result$kind, "boolean") + expect_false(result$independent) + }) + +test_that("conditional independent data with multiple conditioning variables is not rejected", { + set.seed(42) + z1 = rnorm(N) + z2 = rnorm(N) + z3 = rnorm(N) + noise_x = rnorm(N, sd = 0.1) + noise_y = rnorm(N, sd = 0.1) + x = 0.5 * z1 + 0.5 * z2 + 0.5 * z3 + noise_x + y = 0.5 * z1 + 0.5 * z2 + 0.5 * z3 + noise_y + z = cbind(z1, z2, z3) + + result = pearson_correlation_test(x, y, z, FALSE, 0.05) + expect_equal(result$kind, "pvalue") + expect_true(result$p_value >= 0.05) + expect_true(abs(result$coefficient) <= 0.1) + }) + +test_that("minimum input (n=3) with perfect correlation returns coefficient near 1", { + x = c(1.0, 2.0, 3.0) + y = c(1.0, 2.0, 3.0) + z = matrix(0, nrow = 3, ncol = 0) + + result = pearson_correlation_test(x, y, z, FALSE, 0.05) + expect_equal(result$kind, "pvalue") + expect_true(abs(result$coefficient - 1.0) < 1e-10) + expect_true(result$p_value < 0.05) + }) diff --git a/crates/cir/tests/testthat/test-pearson_equivalence.R b/crates/cir/tests/testthat/test-pearson_equivalence.R new file mode 100644 index 0000000..e69de29 diff --git a/crates/cir/tools/config.R b/crates/cir/tools/config.R new file mode 100644 index 0000000..06d57cd --- /dev/null +++ b/crates/cir/tools/config.R @@ -0,0 +1,112 @@ +# Note: Any variables prefixed with `.` are used for text +# replacement in the Makevars.in and Makevars.win.in + +# check the packages MSRV first +source("tools/msrv.R") + +# check DEBUG and NOT_CRAN environment variables +env_debug <- Sys.getenv("DEBUG") +env_not_cran <- Sys.getenv("NOT_CRAN") + +# check if the vendored zip file exists +vendor_exists <- file.exists("src/rust/vendor.tar.xz") + +is_not_cran <- env_not_cran != "" +is_debug <- env_debug != "" + +if (is_debug) { + # if we have DEBUG then we set not cran to true + # CRAN is always release build + is_not_cran <- TRUE + message("Creating DEBUG build.") +} + +if (!is_not_cran) { + message("Building for CRAN.") +} + +# we set cran flags only if NOT_CRAN is empty and if +# the vendored crates are present. +.cran_flags <- ifelse( + !is_not_cran && vendor_exists, + "-j 2 --offline", + "" +) + +# when DEBUG env var is present we use `--debug` build +.profile <- ifelse(is_debug, "", "--release") +.clean_targets <- ifelse(is_debug, "", "$(TARGET_DIR)") + +# We specify this target when building for webR +webr_target <- "wasm32-unknown-emscripten" + +# here we check if the platform we are building for is webr +is_wasm <- identical(R.version$platform, webr_target) + +# print to terminal to inform we are building for webr +if (is_wasm) { + message("Building for WebR") +} + +# we check if we are making a debug build or not +# if so, the LIBDIR environment variable becomes: +# LIBDIR = $(TARGET_DIR)/{wasm32-unknown-emscripten}/debug +# this will be used to fill out the LIBDIR env var for Makevars.in +target_libpath <- if (is_wasm) "wasm32-unknown-emscripten" else NULL +cfg <- if (is_debug) "debug" else "release" + +# used to replace @LIBDIR@ +.libdir <- paste(c(target_libpath, cfg), collapse = "/") + +# use this to replace @TARGET@ +# we specify the target _only_ on webR +# there may be use cases later where this can be adapted or expanded +.target <- ifelse(is_wasm, paste0("--target=", webr_target), "") + +# add panic exports only for WASM builds +.panic_exports <- ifelse( + is_wasm, + "CARGO_PROFILE_DEV_PANIC=\"abort\" CARGO_PROFILE_RELEASE_PANIC=\"abort\" ", + "" +) + +# read in the Makevars.in file checking +is_windows <- .Platform[["OS.type"]] == "windows" + +# if windows we replace in the Makevars.win.in +mv_fp <- ifelse( + is_windows, + "src/Makevars.win.in", + "src/Makevars.in" +) + +# set the output file +mv_ofp <- ifelse( + is_windows, + "src/Makevars.win", + "src/Makevars" +) + +# delete the existing Makevars{.win/.wasm} +if (file.exists(mv_ofp)) { + message("Cleaning previous `", mv_ofp, "`.") + invisible(file.remove(mv_ofp)) +} + +# read as a single string +mv_txt <- readLines(mv_fp) + +# replace placeholder values +new_txt <- gsub("@CRAN_FLAGS@", .cran_flags, mv_txt) |> + gsub("@PROFILE@", .profile, x = _) |> + gsub("@CLEAN_TARGET@", .clean_targets, x = _) |> + gsub("@LIBDIR@", .libdir, x = _) |> + gsub("@TARGET@", .target, x = _) |> + gsub("@PANIC_EXPORTS@", .panic_exports, x = _) + +message("Writing `", mv_ofp, "`.") +con <- file(mv_ofp, open = "wb") +writeLines(new_txt, con, sep = "\n") +close(con) + +message("`tools/config.R` has finished.") diff --git a/crates/cir/tools/msrv.R b/crates/cir/tools/msrv.R new file mode 100644 index 0000000..59a61ab --- /dev/null +++ b/crates/cir/tools/msrv.R @@ -0,0 +1,116 @@ +# read the DESCRIPTION file +desc <- read.dcf("DESCRIPTION") + +if (!"SystemRequirements" %in% colnames(desc)) { + fmt <- c( + "`SystemRequirements` not found in `DESCRIPTION`.", + "Please specify `SystemRequirements: Cargo (Rust's package manager), rustc`" + ) + stop(paste(fmt, collapse = "\n")) +} + +# extract system requirements +sysreqs <- desc[, "SystemRequirements"] + +# check that cargo and rustc is found +if (!grepl("cargo", sysreqs, ignore.case = TRUE)) { + stop("You must specify `Cargo (Rust's package manager)` in your `SystemRequirements`") +} + +if (!grepl("rustc", sysreqs, ignore.case = TRUE)) { + stop("You must specify `Cargo (Rust's package manager), rustc` in your `SystemRequirements`") +} + +# split into parts +parts <- strsplit(sysreqs, ", ")[[1]] + +# identify which is the rustc +rustc_ver <- parts[grepl("rustc", parts)] + +# perform checks for the presence of rustc and cargo on the OS +no_cargo_msg <- c( + "----------------------- [CARGO NOT FOUND]--------------------------", + "The 'cargo' command was not found on the PATH. Please install Cargo", + "from: https://www.rust-lang.org/tools/install", + "", + "Alternatively, you may install Cargo from your OS package manager:", + " - Debian/Ubuntu: apt-get install cargo", + " - Fedora/CentOS: dnf install cargo", + " - macOS: brew install rust", + "-------------------------------------------------------------------" +) + +no_rustc_msg <- c( + "----------------------- [RUST NOT FOUND]---------------------------", + "The 'rustc' compiler was not found on the PATH. Please install", + paste(rustc_ver, "or higher from:"), + "https://www.rust-lang.org/tools/install", + "", + "Alternatively, you may install Rust from your OS package manager:", + " - Debian/Ubuntu: apt-get install rustc", + " - Fedora/CentOS: dnf install rustc", + " - macOS: brew install rust", + "-------------------------------------------------------------------" +) + +# Add {user}/.cargo/bin to path before checking +new_path <- paste0( + Sys.getenv("PATH"), + ":", + paste0(Sys.getenv("HOME"), "/.cargo/bin") +) + +# set the path with the new path +Sys.setenv("PATH" = new_path) + +# check for rustc installation +rustc_version <- tryCatch( + system("rustc --version", intern = TRUE), + error = function(e) { + stop(paste(no_rustc_msg, collapse = "\n")) + } +) + +# check for cargo installation +cargo_version <- tryCatch( + system("cargo --version", intern = TRUE), + error = function(e) { + stop(paste(no_cargo_msg, collapse = "\n")) + } +) + +# helper function to extract versions +extract_semver <- function(ver) { + if (grepl("\\d+\\.\\d+(\\.\\d+)?", ver)) { + sub(".*?(\\d+\\.\\d+(\\.\\d+)?).*", "\\1", ver) + } else { + NA + } +} + +# get the MSRV +msrv <- extract_semver(rustc_ver) + +# extract current version +current_rust_version <- extract_semver(rustc_version) + +# perform check +if (!is.na(msrv)) { + # -1 when current version is later + # 0 when they are the same + # 1 when MSRV is newer than current + is_msrv <- utils::compareVersion(msrv, current_rust_version) + if (is_msrv == 1) { + fmt <- paste0( + "\n------------------ [UNSUPPORTED RUST VERSION]------------------\n", + "- Minimum supported Rust version is %s.\n", + "- Installed Rust version is %s.\n", + "---------------------------------------------------------------" + ) + stop(sprintf(fmt, msrv, current_rust_version)) + } +} + +# print the versions +versions_fmt <- "Using %s\nUsing %s" +message(sprintf(versions_fmt, cargo_version, rustc_version))