Implement clamp_to
Project: rust-lang/rust | Language: Rust | Concepts: api-design, error-handling, type-systems, refactoring | Difficulty: mid
Link: https://github.com/rust-lang/rust/pull/150075
The problem
Rust's standard library provides methods like min, max, and clamp to restrict values within certain bounds. However, there was no unified ergonomic API to restrict a value to a range using Rust's range syntax (e.g., ..=, .., a..=b) in a generic way. Additionally, handling of floating-point edge cases (like NaN bounds) and trait constraints for such an API needed careful design.
Why it's non-trivial
A naive implementation might:
- Only work for a specific type or range kind, not generically for all orderable types and all relevant range types.
- Fail to handle exclusive ranges or floating-point
NaNedge cases correctly. - Not provide a trait-based extension point for custom types or future extensibility.
- Overlook const-evaluation support and feature gating for unstable APIs.
The approach
The PR introduces a new trait and method, implements it for relevant types, and adds robust error handling for floating-point cases.
1. Introduce the ClampBounds trait
This trait allows types to implement a generic "clamp to range" operation:
+/// Trait for ranges supported by [`Ord::clamp_to`].
+#[unstable(feature = "clamp_bounds", issue = "147781")]
+#[rustc_const_unstable(feature = "clamp_bounds", issue = "147781")]
+pub const trait ClampBounds<T>: Sized {
+ /// The implementation of [`Ord::clamp_to`].
+ fn clamp(self, value: T) -> T
+ where
+ T: [const] Destruct;
+}
This trait is unstable and const-friendly, allowing a value to be clamped to a range. It's designed to be implemented for standard range types.
2. Implement ClampBounds for standard range types
For each range type, implement how clamping should work:
+const impl<T> ClampBounds<T> for RangeFrom<T>
+where
+ T: [const] Ord,
+{
+ fn clamp(self, value: T) -> T
+ where
+ T: [const] Destruct,
+ {
+ value.max(self.start)
+ }
+}
For RangeFrom, the value is clamped to be at least the start.
+const impl<T> ClampBounds<T> for RangeToInclusive<T>
+where
+ T: [const] Ord,
+{
+ fn clamp(self, value: T) -> T
+ where
+ T: [const] Destruct,
+ {
+ value.min(self.end)
+ }
+}
For RangeToInclusive, the value is at most the end.
+const impl<T> ClampBounds<T> for RangeInclusive<T>
+where
+ T: [const] Ord,
+{
+ fn clamp(self, value: T) -> T
+ where
+ T: [const] Destruct,
+ {
+ let (start, end) = self.into_inner();
+ value.clamp(start, end)
+ }
+}
For inclusive ranges, clamp between both bounds.
+const impl<T> ClampBounds<T> for RangeFull {
+ fn clamp(self, value: T) -> T {
+ value
+ }
+}
For .., do nothing.
3. Special handling for floating-point types
Floating-point clamping must panic if any bound is NaN, matching the behavior of clamp:
+macro impl_for_float($t:ty) {
+ ...
+ const impl ClampBounds<$t> for RangeFrom<$t> {
+ fn clamp(self, value: $t) -> $t {
+ assert!(!self.start.is_nan(), "start was NaN");
+ value.max(self.start)
+ }
+ }
+ ...
+ const impl ClampBounds<$t> for RangeInclusive<$t> {
+ fn clamp(self, value: $t) -> $t {
+ let (start, end) = self.into_inner();
+ assert!(start <= end, "start > end, or either was NaN");
+ value.clamp(start, end)
+ }
+ }
+}
This macro ensures that for floating-point ranges, NaN bounds cause a panic, preventing subtle bugs.
4. Add the clamp_to method to Ord and float types
The method delegates to the trait, allowing ergonomic usage:
+ #[must_use]
+ #[inline]
+ #[unstable(feature = "clamp_to", issue = "147781")]
+ fn clamp_to<R>(self, range: R) -> Self
+ where
+ Self: Sized + [const] Destruct,
+ R: [const] ClampBounds<Self>,
+ {
+ range.clamp(self)
+ }
Now any type implementing Ord can use .clamp_to(range).
5. Add tests for edge cases
Tests ensure correct panic behavior for invalid bounds, especially with floats:
+float_test! {
+ name: clamp_to_min_is_nan,
+ attrs: {
+ const: #[cfg(false)],
+ f16: #[should_panic, cfg(target_has_reliable_f16_math)],
+ f32: #[should_panic],
+ f64: #[should_panic],
+ f128: #[should_panic, cfg(target_has_reliable_f128_math)],
+ },
+ test {
+ let _ = Float::ONE.clamp_to(Float::NAN..=1.0);
+ }
+}
This test ensures that passing NaN as a bound panics as expected.
From the review thread
joshtriplett: @Kyuuhachi I responded in the two threads that remain open. With those two addressed, r=me.
tgross35:
It is supported reasonably well on wasm, but everything with LLVM andf128libmfunctions is borked right now on all platforms except in specific circumstances (basically only glibc) https://github.com/llvm/llvm-project/issues/44744. Thetarget_has_reliable_f128_mathgate is meant to guard this, as opposed totarget_has_reliable_f128for the basic functionality which works well.
Takeaways
- Design generic APIs using traits to support multiple types and future extensibility.
- Always handle edge cases explicitly, especially for types like floats where
NaNand invalid bounds can cause subtle bugs. - Feature gating and const support are important for evolving standard library APIs safely and ergonomically.
- Comprehensive testing, including for error conditions, is essential when extending core language features.