Skip to content

Tracking issue for step_trait stabilization #42168

Description

@scottmcm

View all comments

Split off from #27741 because the stabilization path for step_by has moved to being on iterators (#41439), and thus not using the Step trait.

(and probably more)

Activity

  1. added
    B-unstableBlocker: Implemented in the nightly compiler and unstable.
    T-libs-api[DEPRECATED; DO NOT USE]
    on May 23, 2017
  2. added a commit that references this issue on May 23, 2017
  3. added 3 commits that reference this issue on May 24, 2017
  4. added a commit that references this issue on May 26, 2017
  5. added 2 commits that reference this issue on May 26, 2017
  6. scottmcm commented on Jun 9, 2017

    @scottmcm
    MemberAuthor

    Some progress on this in #42534

  7. SimonSapin commented on Jul 5, 2017

    @SimonSapin
    Contributor

    PR #43077 does items 1 and 3 in the original message of this issue.

  8. SimonSapin commented on Jul 6, 2017

    @SimonSapin
    Contributor

    I’ve removed the i128/u128 stuff from my PR because I suspect my mixed-signdeness mixed-width interger arithmetic was buggy. I also massaged the Step trait some more and came up with this:

    /// Supporting trait for allowing ranges of various different types to be iterators.
    #[unstable(feature = "step_trait",
               reason = "recently redesigned",
               issue = "42168")]
    pub trait Step: Clone + PartialOrd + Sized {
        /// Returns the number of steps between two step objects. The count is
        /// inclusive of `start` and exclusive of `end`.
        ///
        /// Returns `None` if it is not possible to calculate `steps_between`
        /// without overflow.
        fn steps_between(start: &Self, end: &Self) -> Option<usize>;
    
        /// “Go forward” (for integers, add) the given number of steps, returning None on overflow.
        fn forward(&self, step_count: usize) -> Option<Self>;
    
        /// “Go backward” (for integers, subtract) the given number of steps, returning None on overflow.
        fn backward(&self, step_count: usize) -> Option<Self>;
    
        /// Modify the given inclusive range so that it becomes empty,
        /// for example by setting it to `1...0`.
        fn make_inclusive_range_empty(range: &mut ops::RangeInclusive<Self>);
    }

    How does it look?

    I’m a bit uncertain of the exact impls for integers, there is up to 6 cases to consider: {smaller, same width, larger} than usize/isize × {signed, unsigned}.

  9. added a commit that references this issue on Jul 8, 2017
  10. SimonSapin commented on Jul 8, 2017

    @SimonSapin
    Contributor

    I’ve tweaked this design some more and opened #43127. I believe that PR fixes every issue I know of around the Step trait.

  11. added
    C-tracking-issueCategory: An issue tracking the progress of sth. like the implementation of an RFC
    on Jul 22, 2017
  12. 70 remaining items

  13. added a commit that references this issue on Apr 7, 2026
  14. added a commit that references this issue on Apr 7, 2026
  15. added a commit that references this issue on Apr 7, 2026
  16. added 2 commits that reference this issue on Jun 11, 2026
  17. added a commit that references this issue on Jun 11, 2026
  18. added
    T-libsRelevant to the library team, which will review and decide on the PR/issue.
    and removed
    T-libs-api[DEPRECATED; DO NOT USE]
    on Aug 12, 2026
  19. RalfJung commented on Sep 1, 2026

    @RalfJung
    Member

    Do we really want to allow forward to saturate? I think that is very confusing and semantically wrong. There is no sense in which 255 is the successor of 255 (on NonZero<u8>). There is a sense in which 0 is the successor of 255 (on u8), so wrapping makes sense -- but saturating does not.

    If 255 has a successor on NonZero<u8>, it would be 1 (that'd be my interpretation of "wrapping" for this type). That would be strictly better than saturating, IMO. But really I think forward should just always panic for such cases where there's no "good" successor: we should allow panic and wrapping, but nothing else. Asking for the successor of 255 on NonZero<u8> is clearly buggy code (it panics in debug builds after all). Falling back to wrapping arithmetic is an accepted trade-off when the performance cost of detecting the bug is too high, but falling back to pretty much nonsensical behavior (such as saturation) is so much worse that I don't think this is a good trade-off.

  20. Techcable commented on Sep 5, 2026

    @Techcable
    Contributor

    Do we really want to allow forward to saturate? I think that is very confusing and semantically wrong. There is no sense in which 255 is the successor of 255 (on NonZero<u8>). There is a sense in which 0 is the successor of 255 (on u8), so wrapping makes sense -- but saturating does not.
    [...]

    I was worried about the performance impact of this, but at least on x86, wrapping behavior takes the same number of instructions as saturating behavior (and both are branchless):

    #[unsafe(no_mangle)]
    fn sat_add_nonzero(x: u32, y: u32) -> u32 {
        // add     edi, esi
        // mov     eax, -1
        // cmovae  eax, edi
        // ret
        x.saturating_add(y)
    }
    
    #[unsafe(no_mangle)]
    fn wrapping_add_nonzero(x: u32, y: u32) -> u32 {
        // mov     eax, edi
        // add     eax, esi
        // adc     eax, 0
        // ret
        let (res, ovfl) = x.overflowing_add(y);
        // an if branch here generates the same code
        core::hint::select_unpredictable(ovfl, res.wrapping_add(1), res)
    }

    See on godbolt: https://godbolt.org/z/qbxdfoGWG

  21. khoover commented on Sep 5, 2026

    @khoover

    I think it's overly strict on the allowed behaviours when given an invalid increment. To me, it's a safe forward_unchecked more than an unwrapped forward_checked, where you replace "UB" in the definition of forward_unchecked with "panics or returns an arbitrary value."

    In defense of saturation, though, for NonZero<u8> the addc works if the increment is at most 256. But it can be any usize, what happens when it wraps two or three times? Rather than figuring out mod 255, if forward always saturates it doesn't matter how many times your increment would wrap, the answer is always 255. This holds for other bounded integer types too, e.g. a GridIndex(u8) that validates the input number is in [0, 99]; rather than calculate mod 100, can just saturate at 99 with a sub and cmov.

  22. RalfJung commented on Sep 6, 2026

    @RalfJung
    Member

    "returns an arbitrary value" is a great way to break programs. That feels like a gross violation of the usual principles of API design that the standard library follows.

    Lucky enough, the spec is nowhere near as permissive as that. But it allows "saturation", and that's already going too far IMO.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-iteratorsArea: IteratorsB-unstableBlocker: Implemented in the nightly compiler and unstable.C-tracking-issueCategory: An issue tracking the progress of sth. like the implementation of an RFCI-libs-radarLibs issues that are tracked on the team's radar.T-libsRelevant to the library team, which will review and decide on the PR/issue.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions