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.