Adding a signed integer to an unsigned integer

Forgive me for such a simple question, but why can’t I do this?

pub fn seek(address: u16, amount: i16) u16 {
    return address +% amount;
}

test {
    std.testing.expectEqual(@as(u16, 3), seek(0, 3));
}

jeff@jeff-debian:~/repos/zecm$ zig test src/sii.zig 
src/sii.zig:538:20: error: incompatible types: 'u16' and 'i16'
    return address +% amount;
           ~~~~~~~~^~~~~~~~~
src/sii.zig:538:12: note: type 'u16' here
    return address +% amount;
           ^~~~~~~
src/sii.zig:538:23: note: type 'i16' here
    return address +% amount;

I see a similar example in

but this seems like a lot of code to write for this.

1 Like

It may be worth mentioning that the abs_amt = @abs(amt) becomes unsigned. The @abs returns an unsigned amount for signed integers: Documentation - The Zig Programming Language

One reason why is that this prevents overflow when a range of a signed integer is asymmetric… for example [-128,127] for instance. It can’t map -128 to positive 128 because it will overflow by one. In this case, adding that extra bit (what was originally the sign bit in the case of an unsigned integer of the same bitwidth) will more than compensate for that.

If you need to go from unsigned to signed and always guarentee that the cast will work, you can use an unsigned integer with one less bit and cast to a signed integer with one more bit.

u7 can safely cast to i8
u31 can safely cast to i32
etc...

And then using @intCast, increase the bit width of the unsigned value to the signed target and then do your arithmetic that way.

As you’re noticing, Zig makes it painful (not terribly, but noticeable) on purpose. There’s a lot of issues with mixing lanes and Zig makes those apparent.

4 Likes

related topic:

1 Like

So C has a bunch of implicit conversion rules. It’s supposed to make arithmetic expressions easy to write, and it does do that. But it’s a notorious source of bugs. The actual rules are quite complex, the results can be surprising, and it’s far too easy to hit undefined behavior.

Zig made a different choice, it has exactly one rule:

Type coercions are only allowed when it is completely unambiguous how to get from one type to another, and the transformation is guaranteed to be safe.

This is much better, and also it can be extremely annoying. You’ve hit the case I find most annoying: addition or subtraction between ‘peer’ signed and unsigned types.

This is legal:

fn unsignedMinus(a: usize, b: usize) usize {
    return a - b;
}

But this is not:

fn signedUnsignedPlus(a: usize, b: isize) usize {
    return a + b;
}

That’s despite the fact that both of these functions hold the same hazard: the returned value might be negative. The second one also poses a risk of overflow, but of course it’s easy to run that risk with two usize as well.

Just because it’s annoying doesn’t mean I disagree with it. Adding more rules to make writing code more ergonomic adds back some of the complexity we’re trying to get away from.

But I have this function in probably a majority of my libraries:

inline fn cast(T: type, v: anytype) T {
    return @intCast(v);
}

I think this is central enough to doing useful things with integer values in Zig that it should probably be a builtin. Before result location semantics, @intCast used to take two arguments, and this is exactly how it worked. Writing @as(usize, @intCast(v)) is very heavyweight and ends up obscuring the equation, arithmetic bugs aren’t type conversion bugs, but they’re still bugs.

2 Likes