Inclusive ranges with RangeFrom (making 0.. work like 0..=255 for u8)


#1

Hi language experts,

Another little question about the implementation of ranges in Rust, specifically the newly stabilized inclusive range syntax 0..=255 (from RFC-1192).

Rust already has intervals with no explicit upper limit. They take the form 0.. and is represented by RangeFrom<T>, see range expressions. Given that, I had expected 0.. to be the same as the new 0..=255 when iterating over u8. However, it turns out that the iterator for RangeFrom doesn’t check for overflows! So this program crashes in debug builds (and loops endlessly in release builds):

let range_from: std::ops::RangeFrom<u8> = 0..;
println!("range_from: {:?}", range_from.last());

The equivalent snippet with RangeInclusive works okay (see the playground):

let range_inclusive: std::ops::RangeInclusive<u8> = 0..=255;
println!("range_inclusive: {:?}", range_inclusive.last());

Now, luckily the documentation for RangeFrom says:

Note: Currently, no overflow checking is done for the Iterator implementation; if you use an integer range and the integer overflows, it might panic in debug mode or create an endless loop in release mode. This overflow behavior might change in the future.

Does this mean that it would be possible to change the implementation of the integer iterators to simply count up to a value x where x <= T::max_value() for an integer type T?

With such an implementation, the only reason to have the ..= syntax seems to be for cases where the upper bound is a variable. When the upper bound is known, the programmer can simply write 100..200 or 200.. depending on how high the upper bound is.

Yes, the programmer will still need to be concious of the overflow and cannot write 0..256, but that is something the compiler will detect at compile time. The advantage would be to make a very reasonable loop like for i in 0.. { println!("i: {}", i) } work when i is an u8.

Thoughts?


I see that next is implemented for RangeFrom as follows:

fn next(&mut self) -> Option<A> {
    let mut n = self.start.add_one();
    mem::swap(&mut n, &mut self.start);
    Some(n)
}

I guess my proposal of doing if self.start < T::max_value() { ... } would mean more branches and would require an explicit flag – so the iterator would use more memory. I looked very briefly at the evolution of that chunk of code, but didn’t see any explicit argument for why it looks the way it does today.


#2

Personally speaking, when I omit the upper bound, I do not expect it to overflow… so if it does overflow, then I would want it to panic in debug mode.


#3

What I’m proposing is to make the RangeFrom<u8> iterator generate a finite sequence: today it panics in debug mode and cycles endlessly in release mode. Similarly for other integer iterators.

I’m not sure if that contradicts your expectations above?


#4

It does. I suppose that what I should have said is: When I omit the upper bound, I do not expect the program to successfully exit the loop except through a break, return, or et cetera. Had I known this would not be the case, there’s some code that I might have written more defensively, for the sake of debugging.

But you know what? Forget it, because after thinking about it, I never use a u8 or u16 iteration counter, and maxing out a u32 takes long enough to notice.


#5

Overflow behavior doesn’t seem right at all. Having defined upper bound seems sensible.

for i in 0u8.. is a bit weird, and unclear it goes to 255, but maybe this could be just dismissed as bad style. It could still be useful in some_function(0u8..) that could give it clearer meaning.

for i in 0.. looks like an infinite loop to me, but with a 64-bit usize it’s infinite enough :slight_smile:


#6

I think one should 0..=u8::MAX if that’s desired.

If Iterator used a Result with an error type instead of an Option, I’d say that <RangeFrom as Iterator>::Error would be !, because it’s conceptually infinite, aka it only returns Ok, never Err. (Panicking doesn’t change this; same as repeat().map(|_| panic!()) is “infinite” even though it always panicks.)

It does seem plausible that 0.. should always panic on overflow, and perhaps Wrapping(0).. could be added to do the wrapping behaviour.