Why are range variables always usize?

The language spec recommends using range syntax to iterate over ranges and gives the following example:

var sum3 : usize = 0;
for (0..5) |i| {
    sum3 += i;
try expect(sum3 == 10);

This works great when you’re working with usize integers, but if you have another integer type it becomes awkward:

const a: isize = 1;
const b: isize = 10;
var sum: isize = 0;
for (a..b) |i| {
    sum += i;   // error: incompatible types: 'isize' and 'usize'
try expect(sum == 45);

And of course in this example you could use @intCast(), but I could also imagine situations where the lower bound can be a negative integer, or where bounds are u64 or something.

Is there a reason why the the type of i isn’t simply @TypeOf(a, b)?

The for loop mainly exists to iterate over slices. For example:

const slice: []u8 = ...
for(slice[a..b], a..b) |val, index| {...}

That’s why for(a..b) has the same restrictions as slice[a..b], that being:

  • b must be greater than or equal to a
  • a and b must fit inside a usize (→ only positive values are allowed)

Note that your example only works like this because a and b are comptime known. If they are runtime-known isize you will get an earlier compiler error:

var a: isize = 1;
_ = &a;
var b: isize = 10;
_ = &b;
var sum: isize = 0;
for (a..b) |i| { // error: expected type 'usize', found 'isize'
	sum += i;

So the lower bound can’t be negative.

Here is a proposal for that on github: Proposal: Infer type on ranged for loops from the type of the range values instead of defaulting to usize · Issue #14704 · ziglang/zig · GitHub
It was rejected by Andrew due to implementation problems.