From f44b74b49acf08f80694baf286a0ce781bc4ce9a Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 5 Sep 2026 19:58:20 +0000 Subject: [PATCH 1/4] Pin toolchain to a fixed channel and add an enforced lint gate rust-toolchain.toml floated on "stable", so the pinned toolchain silently retoolchains on every stable release. Pin it to a fixed channel with an append-only bump-log comment, joining the workspace-wide toolchain sweep. Add .github/workflows/lint.yml with two jobs (format, clippy) that install the toolchain from rust-toolchain.toml (no separate toolchain input, so the version lives in exactly one place) and run cargo clippy --all-targets -- -D warnings and cargo fmt --all --check as real gates. crates.yml's existing clippy/fmt steps are left as-is (advisory clippy, no -D warnings) with a one-line pointer comment to the new enforced gate. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01AGVLyRZNEKKBSfBDJfbY3V --- .github/workflows/crates.yml | 2 ++ .github/workflows/lint.yml | 43 ++++++++++++++++++++++++++++++++++++ rust-toolchain.toml | 6 ++++- 3 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/lint.yml diff --git a/.github/workflows/crates.yml b/.github/workflows/crates.yml index 9fc3d00d2e..0471bf5958 100644 --- a/.github/workflows/crates.yml +++ b/.github/workflows/crates.yml @@ -160,11 +160,13 @@ jobs: with: save-if: ${{ github.ref == 'refs/heads/main' }} + # Advisory only (no -D warnings) — the enforced gate lives in lint.yml. - run: rustup component add clippy && cargo clippy --all-targets - name: clippy (bench-suite feature) run: cargo clippy -p tract-cli --features bench-suite - name: test (bench-suite feature) run: cargo test -p tract-cli --features bench-suite --bin tract bench_ + # Enforced fmt check also runs in lint.yml; kept here too for this job's own gating. - name: fmt run: rustup component add rustfmt --toolchain stable && cargo +stable fmt --check - name: Warnings diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000000..16740baed8 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,43 @@ +name: lint + +on: + pull_request: + push: + branches: + - main + +permissions: + contents: read + +concurrency: + group: lint-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + format: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + # No `toolchain:` input — the action reads `rust-toolchain.toml`, so the + # pinned version lives in exactly ONE place. + components: rustfmt, clippy + + - run: cargo fmt --all --check + + clippy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + # No `toolchain:` input — the action reads `rust-toolchain.toml`, so the + # pinned version lives in exactly ONE place. + components: rustfmt, clippy + + - uses: Swatinem/rust-cache@v2 + + - run: cargo clippy --all-targets -- -D warnings diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 73cb934de4..6990df5a6b 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,3 +1,7 @@ +# Bump log (append-only): +# - 2026-09-05: pinned to a fixed channel (was floating "stable"), joining +# the workspace-wide Rust toolchain sweep. A future bump edits only the +# `channel` line below — the version lives in exactly one place. [toolchain] -channel = "stable" +channel = "1.98.1" components = ["rustfmt", "clippy"] From 29cecc71a4d527ca1bdb1c862cc5c24d9e9e827a Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 5 Sep 2026 20:05:42 +0000 Subject: [PATCH 2/4] Bump the declared MSRV to 1.98 and pin the gate's actions Two failures on the first CI run of this PR, one of which is not this PR's and one of which is. Every `ubuntu-latest / / 1.97` job failed with error: rustc 1.97.1 is not supported by the following package: ndarray@0.17.2 requires rustc 1.98 That is not the toolchain file this PR touched. The matrix derives its Rust version from `Cargo.toml`'s own `rust-version` and exports it as `RUSTUP_TOOLCHAIN`, which overrides `rust-toolchain.toml` entirely -- so those jobs ran on 1.97 before and after this PR alike. What changed is upstream: this workspace patches `ndarray` from the fork's master branch, and that fork just raised its own declared MSRV to 1.98. Every consumer still declaring 1.97 goes red the moment it resolves. Bumping `rust-version` here is the same toolchain move this PR already makes, applied to the file CI actually reads. The zizmor audit is this PR's. The workflow used floating action tags and left checkout credentials persisted, against the repo's blanket policy. Rewritten in the repo's own idiom instead: the two actions it still needs are pinned to the same commits the existing workflows use, checkout sets `persist-credentials: false`, and the toolchain comes from `rustup show` reading `rust-toolchain.toml` -- which drops the third-party toolchain action altogether and keeps the version in one place. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01AGVLyRZNEKKBSfBDJfbY3V --- .github/workflows/lint.yml | 38 +++++++++++++++++++++++++------------- Cargo.toml | 2 +- 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 16740baed8..631dc0c7bd 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -13,31 +13,43 @@ concurrency: group: lint-${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +env: + CARGO_INCREMENTAL: false + CARGO_NET_RETRY: 10 + jobs: format: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - - uses: actions-rust-lang/setup-rust-toolchain@v1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - # No `toolchain:` input — the action reads `rust-toolchain.toml`, so the - # pinned version lives in exactly ONE place. - components: rustfmt, clippy + persist-credentials: false + + # `rustup show` installs and activates whatever `rust-toolchain.toml` + # pins, so the version lives in exactly ONE place. This is also the + # repo's own idiom — the other workflows drive rustup directly rather + # than through a third-party toolchain action. + - name: Setup rust toolchain (pinned by rust-toolchain.toml) + run: | + rustup show + rustup component add rustfmt - run: cargo fmt --all --check clippy: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - - uses: actions-rust-lang/setup-rust-toolchain@v1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - # No `toolchain:` input — the action reads `rust-toolchain.toml`, so the - # pinned version lives in exactly ONE place. - components: rustfmt, clippy + persist-credentials: false + + - name: Setup rust toolchain (pinned by rust-toolchain.toml) + run: | + rustup show + rustup component add clippy - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 + with: + save-if: ${{ github.ref == 'refs/heads/main' }} - run: cargo clippy --all-targets -- -D warnings diff --git a/Cargo.toml b/Cargo.toml index 8264ba49b3..f01277014e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -124,7 +124,7 @@ default-members = [ [workspace.package] # MSRV source of truth: CI derives the tested toolchain from this. Keep the # README rustc badge in sync when bumping. -rust-version = "1.97" +rust-version = "1.98" [workspace.dependencies] anstyle = "1.0.2" From 8901cb2d3ea40fbcc9433421c72c1b41f195c2ac Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 5 Sep 2026 20:06:08 +0000 Subject: [PATCH 3/4] Sync the README rustc badge with the MSRV bump The MSRV declaration in Cargo.toml carries a standing instruction to keep this badge in sync; the previous commit moved the floor to 1.98 and left the badge behind. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01AGVLyRZNEKKBSfBDJfbY3V --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 655556c04c..4fa5ac421e 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ ![tract-logo](assets/tract-logo/PNG/tract-horizontal-blue.png) ![Rust](https://img.shields.io/badge/rust-%23000000.svg?style=for-the-badge&logo=rust&logoColor=white) -![rustc >= 1.97.0](https://img.shields.io/badge/rustc-%3E%3D1.97.0-brightgreen) +![rustc >= 1.98.0](https://img.shields.io/badge/rustc-%3E%3D1.98.0-brightgreen) ![MIT/Apache 2](https://img.shields.io/crates/l/tract) [![Native Linux test status](https://github.com/sonos/tract/workflows/Native%20Linux/badge.svg)](https://github.com/sonos/tract/actions) [![Embedded targets status](https://github.com/sonos/tract/workflows/Embedded%20targets/badge.svg)](https://github.com/sonos/tract/actions) From e1a8d3878c48244ee884ccc3fd531b24ed312a73 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 5 Sep 2026 20:31:48 +0000 Subject: [PATCH 4/4] Clear the linear-algebra findings the new gate reports Nine findings, and the enumeration is complete: a local run of the same command the gate uses reports nothing else across the workspace. Seven are code. Six are needless borrows on `Some(&x.name.as_str())`, where the reference is created and immediately dereferenced again. The seventh is `chunks_exact_mut(4)` in the accurate-exponential reduction, rewritten as `as_chunks_mut::<4>()`, which also retires the `into_remainder()` dance: the remainder now falls out of the same call that produces the chunks. Two are argument counts, and both are allowed rather than refactored. In the hardware benchmark the arguments ARE the benchmarked shape, and in the recurrent-operator test helper they are the layer's own inputs. Grouping either set into a struct would name the same values one indirection away and make the call sites harder to read, not easier. Each allow carries that reason. Verified with the gate's own commands: `cargo clippy --all-targets -- -D warnings` and `cargo fmt --all -- --check`, both clean. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01AGVLyRZNEKKBSfBDJfbY3V --- cli/src/hwbench.rs | 4 ++++ linalg/src/arm32/mod.rs | 10 +++++----- linalg/src/generic/reduce.rs | 6 +++--- linalg/src/x86_64/mmm.rs | 2 +- transformers/src/ops/gdn_recurrent.rs | 4 ++++ 5 files changed, 17 insertions(+), 9 deletions(-) diff --git a/cli/src/hwbench.rs b/cli/src/hwbench.rs index f899c25143..f0d77db87d 100644 --- a/cli/src/hwbench.rs +++ b/cli/src/hwbench.rs @@ -385,6 +385,10 @@ fn llc_bytes() -> usize { max.unwrap_or(32 * 1024 * 1024) } +// The arguments ARE the benchmarked shape — datum type plus the GEMM's own +// dimensions. Folding them into a parameter struct would name the same eight +// values one indirection away without making any call site clearer. +#[allow(clippy::too_many_arguments)] fn bench_shape( dt: DatumType, m: usize, diff --git a/linalg/src/arm32/mod.rs b/linalg/src/arm32/mod.rs index 9309b5778f..6a4591721c 100644 --- a/linalg/src/arm32/mod.rs +++ b/linalg/src/arm32/mod.rs @@ -47,15 +47,15 @@ fn neon_mmv_f32(suitable: &[Suitable], query: &Query) -> Option<&'static str> { Some(m) if m < 32 => { cortex_a7_mmv_linear::linear_model().preferred(suitable, Some(m), query.k, Some(1)) } - _ => Some(&armv7neon::armv7neon_mmm_f32_32x1_cortexa7.name.as_str()), + _ => Some(armv7neon::armv7neon_mmm_f32_32x1_cortexa7.name.as_str()), }, 0xc09 => match query.m { Some(m) if m < 32 => { cortex_a9_mmv_linear::linear_model().preferred(suitable, Some(m), query.k, Some(1)) } - _ => Some(&armv7neon::armv7neon_mmm_f32_32x1_cortexa9.name.as_str()), + _ => Some(armv7neon::armv7neon_mmm_f32_32x1_cortexa9.name.as_str()), }, - _ => Some(&armv7neon::armv7neon_mmm_f32_32x1_generic.name.as_str()), + _ => Some(armv7neon::armv7neon_mmm_f32_32x1_generic.name.as_str()), } } @@ -80,8 +80,8 @@ fn preferred( match (dt, query.n) { (DatumType::F32, Some(1)) => neon_mmv_f32(suitable, query), (DatumType::F32, _) => neon_mmm_f32(suitable, query), - (DatumType::I32, Some(1)) => Some(&armv7neon::armv7neon_mmm_i32_32x1.name.as_str()), - (DatumType::I32, _) => Some(&armv7neon::armv7neon_mmm_i32_8x4.name.as_str()), + (DatumType::I32, Some(1)) => Some(armv7neon::armv7neon_mmm_i32_32x1.name.as_str()), + (DatumType::I32, _) => Some(armv7neon::armv7neon_mmm_i32_8x4.name.as_str()), _ => None, } } diff --git a/linalg/src/generic/reduce.rs b/linalg/src/generic/reduce.rs index 8843526096..8a2ce184de 100644 --- a/linalg/src/generic/reduce.rs +++ b/linalg/src/generic/reduce.rs @@ -227,8 +227,8 @@ pub mod softmax_l2 { #[inline] fn exp_sum_impl(x: &mut [f32], max: f32) -> f32 { let mut acc = [0f32; 4]; - let mut it = x.chunks_exact_mut(4); - for c in &mut it { + let (chunks, remainder) = x.as_chunks_mut::<4>(); + for c in chunks.iter_mut() { for (j, v) in c.iter_mut().enumerate() { let y = accurate_exp_f32(*v - max); *v = y; @@ -236,7 +236,7 @@ pub mod softmax_l2 { } } let mut sum = (acc[0] + acc[1]) + (acc[2] + acc[3]); - for v in it.into_remainder().iter_mut() { + for v in remainder.iter_mut() { let y = accurate_exp_f32(*v - max); *v = y; sum += y; diff --git a/linalg/src/x86_64/mmm.rs b/linalg/src/x86_64/mmm.rs index 5a9b1a1e41..094cb90039 100644 --- a/linalg/src/x86_64/mmm.rs +++ b/linalg/src/x86_64/mmm.rs @@ -490,7 +490,7 @@ fn avx2_preferred( ) -> Option<&'static str> { match (dt, query.n) { (DatumType::I32, Some(1)) => None, - (DatumType::I32, _) => Some(&mmm::avx2_mmm_i32_8x8.name.as_str()), + (DatumType::I32, _) => Some(mmm::avx2_mmm_i32_8x8.name.as_str()), _ => None, } } diff --git a/transformers/src/ops/gdn_recurrent.rs b/transformers/src/ops/gdn_recurrent.rs index 647ab7b8d3..4a84a27475 100644 --- a/transformers/src/ops/gdn_recurrent.rs +++ b/transformers/src/ops/gdn_recurrent.rs @@ -211,6 +211,10 @@ mod tests { use super::*; use crate::ops::test_utils::arb; + // A test helper whose arguments are the layer's own inputs: three shape + // values and the six tensors the operator consumes. Grouping them would + // obscure exactly what each case is feeding in. + #[allow(clippy::too_many_arguments)] fn run( s_len: usize, heads: usize,