diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b5f4d11a..40380545 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -113,6 +113,25 @@ jobs: - name: Check dependencies run: cargo machete && cargo deny check advisories + miri: + if: ${{ github.server_url == 'https://github.com' }} + name: Miri + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6 + + # The existence tests drive the dense, usize-keyed count state. A pinned + # nightly keeps a new nightly from failing CI on its own; bump it on + # purpose. + - name: Install Miri + run: rustup toolchain install nightly-2026-01-20 --profile minimal --component miri --no-self-update + + - name: Run the existence tests under Miri + run: | + cargo +nightly-2026-01-20 miri setup + cargo +nightly-2026-01-20 miri test -p solverforge-scoring --lib -- constraint::tests::exists + package: if: ${{ github.server_url == 'https://github.com' }} name: Package diff --git a/crates/solverforge-scoring/src/lib.rs b/crates/solverforge-scoring/src/lib.rs index 42149e00..0c3e6c05 100644 --- a/crates/solverforge-scoring/src/lib.rs +++ b/crates/solverforge-scoring/src/lib.rs @@ -18,6 +18,7 @@ #![allow(clippy::type_complexity)] #![warn(unreachable_pub)] #![warn(missing_docs)] +#![forbid(unsafe_code)] // Core modules pub mod api; diff --git a/crates/solverforge-scoring/src/stream/relational/count_state.rs b/crates/solverforge-scoring/src/stream/relational/count_state.rs index dc6dea71..244ed52b 100644 --- a/crates/solverforge-scoring/src/stream/relational/count_state.rs +++ b/crates/solverforge-scoring/src/stream/relational/count_state.rs @@ -11,6 +11,8 @@ //! once at construction; the event path calls one enum method per key and //! nothing else branches. +use std::any::Any; + use super::{FastMap, RowHandle}; /// One key's count plus the epoch of its last touch, stamped against the @@ -291,26 +293,27 @@ impl DenseKeys { } /// View a generic key as its `usize` representation. Only called inside the -/// `K == usize` arm of `KeyState::new`, where the conversion is the identity. +/// `K == usize` arm of `KeyState::new`, where the downcast is the identity and +/// monomorphizes to a plain read. +/// +/// # Panics +/// +/// When `K` is not `usize`. fn dense_raw(key: &K) -> usize { - debug_assert_eq!( - std::any::TypeId::of::(), - std::any::TypeId::of::(), - "dense counts are usize-keyed" - ); - // SAFETY: K is usize in this arm; the reference reinterprets the same - // non-null, aligned, initialized 8 bytes. - unsafe { *(key as *const K as *const usize) } + *(key as &dyn Any) + .downcast_ref::() + .expect("dense counts are usize-keyed") } /// Inverse of `dense_raw`, same `K == usize` arm. +/// +/// # Panics +/// +/// When `K` is not `usize`. fn key_from_raw(raw: usize) -> K { - debug_assert_eq!( - std::any::TypeId::of::(), - std::any::TypeId::of::(), - "dense counts are usize-keyed" - ); - // SAFETY: K is usize in this arm; usize is repr-transparent `K` here and - // the pointer is valid, aligned, initialized. - unsafe { std::ptr::read(&raw as *const usize as *const K) } + let mut slot = Some(raw); + (&mut slot as &mut dyn Any) + .downcast_mut::>() + .and_then(Option::take) + .expect("dense counts are usize-keyed") }