Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Changed

- `from_parts_unchecked` on `CumulativeROHistogramRef` /
`CumulativeROHistogram32Ref` / `SparseHistogramRef` /
`SparseHistogram32Ref` now runs the same validation as `from_parts`
inside a `debug_assert!`. Debug builds catch invariant violations at
the call site; release builds are unchanged (validation is elided).

## [1.3.1] - 2026-04-29

### Added
Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "histogram"
version = "1.3.1"
version = "1.3.2-alpha.0"
edition = "2024"
authors = ["Brian Martin <brian@iop.systems>", "Yao Yue <yao@iop.systems>"]
license = "MIT OR Apache-2.0"
Expand Down
56 changes: 54 additions & 2 deletions src/cumulative.rs
Original file line number Diff line number Diff line change
Expand Up @@ -360,15 +360,30 @@ macro_rules! define_cumulative_histogram {

/// Creates a borrowed view without validating invariants.
///
/// # Safety
/// Skips the O(n) validation pass that `from_parts` performs.
/// Intended for hot paths where the caller already guarantees the
/// invariants (e.g. data produced by this crate or validated once
/// at load time).
///
/// # Contract
///
/// Caller must ensure `index` and `count` satisfy the same invariants
/// as the owned `from_parts`.
/// as the owned `from_parts`. Misuse produces incorrect quantile
/// output rather than memory unsafety, which is why this is a safe
/// `fn`. In debug builds, the invariants are checked via
/// `debug_assert!`; release builds skip the check entirely.
pub fn from_parts_unchecked(
config: Config,
index: &'a [u32],
count: &'a [$count],
) -> Self {
debug_assert!(
Self::validate(&config, index, count).is_ok(),
concat!(
stringify!($ref_name),
"::from_parts_unchecked called with invalid inputs"
),
);
Self {
config,
index,
Expand Down Expand Up @@ -1130,4 +1145,41 @@ mod tests {
SampleQuantiles::quantiles(&owned, qs).unwrap()
);
}

// Debug-only safety net: `from_parts_unchecked` should panic in debug
// builds when handed inputs that violate the invariants. Release builds
// skip the check entirely (no panic), so these tests only run when
// debug assertions are enabled.

#[cfg(debug_assertions)]
#[test]
#[should_panic(expected = "from_parts_unchecked called with invalid inputs")]
fn ref_from_parts_unchecked_debug_panics_on_length_mismatch() {
let config = Config::new(7, 32).unwrap();
let _ = CumulativeROHistogramRef::from_parts_unchecked(config, &[1, 3], &[10]);
}

#[cfg(debug_assertions)]
#[test]
#[should_panic(expected = "from_parts_unchecked called with invalid inputs")]
fn ref_from_parts_unchecked_debug_panics_on_non_ascending_indices() {
let config = Config::new(7, 32).unwrap();
let _ = CumulativeROHistogramRef::from_parts_unchecked(config, &[3, 1], &[10u64, 40]);
}

#[cfg(debug_assertions)]
#[test]
#[should_panic(expected = "from_parts_unchecked called with invalid inputs")]
fn ref_from_parts_unchecked_debug_panics_on_decreasing_counts() {
let config = Config::new(7, 32).unwrap();
let _ = CumulativeROHistogramRef::from_parts_unchecked(config, &[1, 3], &[40u64, 10]);
}

#[cfg(debug_assertions)]
#[test]
#[should_panic(expected = "from_parts_unchecked called with invalid inputs")]
fn ref32_from_parts_unchecked_debug_panics_on_zero_count() {
let config = Config::new(7, 32).unwrap();
let _ = CumulativeROHistogram32Ref::from_parts_unchecked(config, &[1], &[0u32]);
}
}
19 changes: 17 additions & 2 deletions src/sparse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -466,15 +466,30 @@ macro_rules! define_sparse_histogram {

/// Creates a borrowed view without validating invariants.
///
/// # Safety
/// Skips the O(n) validation pass that `from_parts` performs.
/// Intended for hot paths where the caller already guarantees the
/// invariants (e.g. data produced by this crate or validated once
/// at load time).
///
/// # Contract
///
/// Caller must ensure `index` and `count` satisfy the same invariants
/// as the owned `from_parts`.
/// as the owned `from_parts`. Misuse produces incorrect quantile
/// output rather than memory unsafety, which is why this is a safe
/// `fn`. In debug builds, the invariants are checked via
/// `debug_assert!`; release builds skip the check entirely.
pub fn from_parts_unchecked(
config: Config,
index: &'a [u32],
count: &'a [$count],
) -> Self {
debug_assert!(
Self::validate(&config, index, count).is_ok(),
concat!(
stringify!($ref_name),
"::from_parts_unchecked called with invalid inputs"
),
);
Self {
config,
index,
Expand Down
Loading