From 5085b81241a89b119b96bc13b4350e134bfaaaac Mon Sep 17 00:00:00 2001 From: Adam Kern Date: Sat, 6 Jun 2026 13:58:22 -0400 Subject: [PATCH 1/4] fix: permute_axes bug As pointed out in #1589, permute_axes had a bug. This uses a simple algorithm to fix it; explanations for this algorithm are all over the internet, it's nothing fancy. I did take the opportunity to introduce proptest, which I'd like to make more use of over time. Down the line, it would be good to change the argument to `permute_axes` to be something like `T: Permutation`. Right now, `permute_axes` just has an assertion that the input is a real permutation. I'd call that a classic example of this library opting for asserts when we could be using better typing. But I think that will be easier / better to introduce after the core rework. --- Cargo.lock | 108 ++++++++++++++++++++++++++ Cargo.toml | 2 + proptest-regressions/impl_methods.txt | 7 ++ src/impl_methods.rs | 59 ++++++++------ 4 files changed, 153 insertions(+), 23 deletions(-) create mode 100644 proptest-regressions/impl_methods.txt diff --git a/Cargo.lock b/Cargo.lock index a0e829273..de366de05 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -47,6 +47,21 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[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.13.0" @@ -245,6 +260,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + [[package]] name = "filetime" version = "0.2.29" @@ -271,6 +292,12 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "foldhash" version = "0.1.5" @@ -474,6 +501,7 @@ dependencies = [ "num-traits", "portable-atomic", "portable-atomic-util", + "proptest", "quickcheck", "rawpointer", "rayon", @@ -663,6 +691,31 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "proptest" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" +dependencies = [ + "bit-set", + "bit-vec", + "bitflags", + "num-traits", + "rand 0.9.4", + "rand_chacha", + "rand_xorshift", + "regex-syntax", + "rusty-fork", + "tempfile", + "unarray", +] + +[[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.1.0" @@ -757,6 +810,15 @@ dependencies = [ "rand_core 0.9.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.5", +] + [[package]] name = "rawpointer" version = "0.2.1" @@ -794,6 +856,12 @@ dependencies = [ "thiserror", ] +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + [[package]] name = "ring" version = "0.17.14" @@ -890,6 +958,18 @@ dependencies = [ "untrusted", ] +[[package]] +name = "rusty-fork" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" +dependencies = [ + "fnv", + "quick-error", + "tempfile", + "wait-timeout", +] + [[package]] name = "semver" version = "1.0.28" @@ -990,6 +1070,19 @@ dependencies = [ "xattr", ] +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + [[package]] name = "thiserror" version = "2.0.18" @@ -1019,6 +1112,12 @@ dependencies = [ "crossbeam-channel", ] +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + [[package]] name = "unicode-ident" version = "1.0.24" @@ -1077,6 +1176,15 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "wait-timeout" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +dependencies = [ + "libc", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" diff --git a/Cargo.toml b/Cargo.toml index 72bce2855..df07302ea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -57,6 +57,7 @@ quickcheck = { workspace = true } approx = { workspace = true, default-features = true } itertools = { workspace = true } ndarray-gen = { workspace = true } +proptest = { workspace = true } [features] default = ["std"] @@ -103,6 +104,7 @@ num-traits = { version = "0.2", default-features = false } num-complex = { version = "0.4", default-features = false } approx = { version = "0.5", default-features = false } quickcheck = { version = "1.0", default-features = false } +proptest = { version = "1.3.1" } rand = { version = "0.9.0", features = ["small_rng"] } rand_distr = { version = "0.5.0" } itertools = { version = "0.13.0", default-features = false, features = ["use_std"] } diff --git a/proptest-regressions/impl_methods.txt b/proptest-regressions/impl_methods.txt new file mode 100644 index 000000000..15b9dff36 --- /dev/null +++ b/proptest-regressions/impl_methods.txt @@ -0,0 +1,7 @@ +# 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 6df087160c6028416bca035db64caf430391dda0af3b195969776eb715184310 # shrinks to p = [0, 1, 2, 3, 4, 5] diff --git a/src/impl_methods.rs b/src/impl_methods.rs index 2170a8d93..320f6c2d1 100644 --- a/src/impl_methods.rs +++ b/src/impl_methods.rs @@ -10,6 +10,14 @@ use alloc::slice; use alloc::vec; #[cfg(not(feature = "std"))] use alloc::vec::Vec; +#[cfg(test)] +use proptest::prop_assert_eq; +#[cfg(test)] +use proptest::proptest; +#[cfg(test)] +use proptest::strategy::Just; +#[cfg(test)] +use proptest::strategy::Strategy; #[allow(unused_imports)] use rawpointer::PointerExt; use std::mem::{size_of, ManuallyDrop}; @@ -2585,30 +2593,16 @@ where let dim = self.parts.dim.slice_mut(); let strides = self.parts.strides.slice_mut(); - let axes = axes.slice(); - - // The cycle detection is done using a bitmask to track visited positions. - // For example, axes from [0,1,2] to [2, 0, 1] - // For axis values [1, 0, 2]: - // 1 << 1 // 0b0001 << 1 = 0b0010 (decimal 2) - // 1 << 0 // 0b0001 << 0 = 0b0001 (decimal 1) - // 1 << 2 // 0b0001 << 2 = 0b0100 (decimal 4) - // - // Each axis gets its own unique bit position in the bitmask: - // - Axis 0: bit 0 (rightmost) - // - Axis 1: bit 1 - // - Axis 2: bit 2 - // - let mut visited = 0usize; - for (new_axis, &axis) in axes.iter().enumerate() { - if (visited & (1 << axis)) != 0 { - continue; - } - dim.swap(axis, new_axis); - strides.swap(axis, new_axis); - - visited |= (1 << axis) | (1 << new_axis); + for i in 0..axes.ndim() { + let mut index = axes[i]; + while index < i { + index = axes[index]; + } + if index != i { + dim.swap(i, index); + strides.swap(i, index); + } } } @@ -3614,4 +3608,23 @@ mod tests let result_slice = empty_slice.partition(0, Axis(0)); assert_eq!(result_slice.shape(), &[0, 3]); } + + /// Regression test for permute_axes + #[test] + fn test_permute_axes_regression() + { + let mut a = Array4::::zeros((1, 2, 3, 4)); + a.permute_axes([3, 0, 1, 2]); + assert_eq!(a.shape(), &[4, 1, 2, 3]); + } +} + +#[cfg(test)] +proptest! { + #[test] + fn test_permute_axes_6d(p in Just([0, 1, 2, 3, 4, 5]).prop_shuffle()) { + let mut arr: Array6 = Array6::zeros((0, 1, 2, 3, 4, 5)); + arr.permute_axes(p.clone()); + prop_assert_eq!(arr.shape(), p); + } } From d311c167edcf8c72b0d0665cdd34bea7c284c0a3 Mon Sep 17 00:00:00 2001 From: Adam Kern Date: Sat, 6 Jun 2026 14:25:09 -0400 Subject: [PATCH 2/4] Satisfy clippy and make proptest optional for msrv --- .github/workflows/ci.yaml | 4 ++-- Cargo.toml | 5 ++++- scripts/all-tests.sh | 5 +++++ src/impl_methods.rs | 11 ++++++----- 4 files changed, 17 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index e359264ef..d475a78aa 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -12,7 +12,7 @@ name: Continuous integration env: CARGO_TERM_COLOR: always HOST: x86_64-unknown-linux-gnu - FEATURES: "approx,serde,rayon" + FEATURES: "approx,serde,rayon,proptest" RUSTFLAGS: "-D warnings" MSRV: 1.87.0 BLAS_MSRV: 1.87.0 @@ -44,7 +44,7 @@ jobs: toolchain: ${{ matrix.rust }} components: clippy - uses: Swatinem/rust-cache@v2 - - run: cargo clippy --features approx,serde,rayon + - run: cargo clippy "${FEATURES}" format: runs-on: ubuntu-latest diff --git a/Cargo.toml b/Cargo.toml index df07302ea..b8e7135fc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -51,13 +51,14 @@ matrixmultiply = { version = "0.3.2", default-features = false, features=["cgemm serde = { version = "1.0", optional = true, default-features = false, features = ["alloc"] } rawpointer = { version = "0.2" } +proptest = { workspace = true, optional = true } + [dev-dependencies] defmac = "0.2" quickcheck = { workspace = true } approx = { workspace = true, default-features = true } itertools = { workspace = true } ndarray-gen = { workspace = true } -proptest = { workspace = true } [features] default = ["std"] @@ -75,6 +76,8 @@ matrixmultiply-threading = ["matrixmultiply/threading"] portable-atomic-critical-section = ["portable-atomic/critical-section"] +proptest = ["dep:proptest"] + [target.'cfg(not(target_has_atomic = "ptr"))'.dependencies] portable-atomic = { version = "1.6.0" } diff --git a/scripts/all-tests.sh b/scripts/all-tests.sh index f6c9b27a8..4e911c9bd 100755 --- a/scripts/all-tests.sh +++ b/scripts/all-tests.sh @@ -8,6 +8,11 @@ CHANNEL=$2 QC_FEAT=--features=ndarray-rand/quickcheck +# Proptest needs 1.85, MSRV is 1.64, so only run proptest tests if MSRV is not set or if we're on a newer channel than MSRV +if [[ -z "${MSRV}" ]] && [ "$CHANNEL" != "$MSRV" ]; then + FEATURES="$FEATURES,proptest" +fi + # build check with no features cargo build -v --no-default-features diff --git a/src/impl_methods.rs b/src/impl_methods.rs index 320f6c2d1..3ac9e675d 100644 --- a/src/impl_methods.rs +++ b/src/impl_methods.rs @@ -10,13 +10,13 @@ use alloc::slice; use alloc::vec; #[cfg(not(feature = "std"))] use alloc::vec::Vec; -#[cfg(test)] +#[cfg(all(test, feature = "proptest"))] use proptest::prop_assert_eq; -#[cfg(test)] +#[cfg(all(test, feature = "proptest"))] use proptest::proptest; -#[cfg(test)] +#[cfg(all(test, feature = "proptest"))] use proptest::strategy::Just; -#[cfg(test)] +#[cfg(all(test, feature = "proptest"))] use proptest::strategy::Strategy; #[allow(unused_imports)] use rawpointer::PointerExt; @@ -3619,7 +3619,8 @@ mod tests } } -#[cfg(test)] +#[cfg(all(test, feature = "proptest"))] +#[cfg_attr(miri, ignore)] proptest! { #[test] fn test_permute_axes_6d(p in Just([0, 1, 2, 3, 4, 5]).prop_shuffle()) { From ee700dee980f5328131692ec4dad5dbef1dbf983 Mon Sep 17 00:00:00 2001 From: Adam Kern Date: Sat, 6 Jun 2026 15:40:53 -0400 Subject: [PATCH 3/4] Remove proptest feature --- .github/workflows/ci.yaml | 2 +- Cargo.toml | 5 +---- scripts/all-tests.sh | 5 ----- src/impl_methods.rs | 10 +++++----- 4 files changed, 7 insertions(+), 15 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index d475a78aa..0084ea448 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -12,7 +12,7 @@ name: Continuous integration env: CARGO_TERM_COLOR: always HOST: x86_64-unknown-linux-gnu - FEATURES: "approx,serde,rayon,proptest" + FEATURES: "approx,serde,rayon" RUSTFLAGS: "-D warnings" MSRV: 1.87.0 BLAS_MSRV: 1.87.0 diff --git a/Cargo.toml b/Cargo.toml index b8e7135fc..df07302ea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -51,14 +51,13 @@ matrixmultiply = { version = "0.3.2", default-features = false, features=["cgemm serde = { version = "1.0", optional = true, default-features = false, features = ["alloc"] } rawpointer = { version = "0.2" } -proptest = { workspace = true, optional = true } - [dev-dependencies] defmac = "0.2" quickcheck = { workspace = true } approx = { workspace = true, default-features = true } itertools = { workspace = true } ndarray-gen = { workspace = true } +proptest = { workspace = true } [features] default = ["std"] @@ -76,8 +75,6 @@ matrixmultiply-threading = ["matrixmultiply/threading"] portable-atomic-critical-section = ["portable-atomic/critical-section"] -proptest = ["dep:proptest"] - [target.'cfg(not(target_has_atomic = "ptr"))'.dependencies] portable-atomic = { version = "1.6.0" } diff --git a/scripts/all-tests.sh b/scripts/all-tests.sh index 4e911c9bd..f6c9b27a8 100755 --- a/scripts/all-tests.sh +++ b/scripts/all-tests.sh @@ -8,11 +8,6 @@ CHANNEL=$2 QC_FEAT=--features=ndarray-rand/quickcheck -# Proptest needs 1.85, MSRV is 1.64, so only run proptest tests if MSRV is not set or if we're on a newer channel than MSRV -if [[ -z "${MSRV}" ]] && [ "$CHANNEL" != "$MSRV" ]; then - FEATURES="$FEATURES,proptest" -fi - # build check with no features cargo build -v --no-default-features diff --git a/src/impl_methods.rs b/src/impl_methods.rs index 3ac9e675d..9187beb6b 100644 --- a/src/impl_methods.rs +++ b/src/impl_methods.rs @@ -10,13 +10,13 @@ use alloc::slice; use alloc::vec; #[cfg(not(feature = "std"))] use alloc::vec::Vec; -#[cfg(all(test, feature = "proptest"))] +#[cfg(test)] use proptest::prop_assert_eq; -#[cfg(all(test, feature = "proptest"))] +#[cfg(test)] use proptest::proptest; -#[cfg(all(test, feature = "proptest"))] +#[cfg(test)] use proptest::strategy::Just; -#[cfg(all(test, feature = "proptest"))] +#[cfg(test)] use proptest::strategy::Strategy; #[allow(unused_imports)] use rawpointer::PointerExt; @@ -3619,7 +3619,7 @@ mod tests } } -#[cfg(all(test, feature = "proptest"))] +#[cfg(test)] #[cfg_attr(miri, ignore)] proptest! { #[test] From 826cce4472be5b0618035d292c9f69e3ad57f8bb Mon Sep 17 00:00:00 2001 From: Adam Kern Date: Sat, 6 Jun 2026 15:42:20 -0400 Subject: [PATCH 4/4] Fix clippy features --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 0084ea448..b9210d391 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -44,7 +44,7 @@ jobs: toolchain: ${{ matrix.rust }} components: clippy - uses: Swatinem/rust-cache@v2 - - run: cargo clippy "${FEATURES}" + - run: cargo clippy -F "${FEATURES}" format: runs-on: ubuntu-latest