Preventing Integer Overflow In Zig

Consider the following Zig program:

const std = @import("std");
pub fn main() void {
    var x: u16 = 500;
    var y: u16 = 500;
    var z: u32 = x * y;
    std.debug.print("{d}\n", .{z});
}

Run it with zig run overflow.zig:

thread 21769477 panic: integer overflow
/Users/ehaas/bugs/test.zig:5:20: 0x105008c97 in main (test)
    var z: u32 = x * y;
                   ^
/Users/ehaas/source/zig/build/lib/zig/std/start.zig:335:22: 0x105008f1d in std.start.main (test)
            root.main();
                     ^
???:?:?: 0x7fff205aa620 in ??? (???)
???:?:?: 0x0 in ??? (???)
[1]    98894 abort      zig run test.zig

500 * 500 is only 250,000 - so what happened? In Zig, the multiplication operator invokes Peer Type Resolution on its operands. In this case, the operands are both u16, so a 16-bit multiply will be performed, resulting in a value of 250000, which is larger than the maximum u16 value of 65535 - thus integer overflow occurs, which causes a panic in safety-checked build modes.

So how do we deal with this? There are least 3 ways, and it depends on what you're trying to do:

1. Use wrapping arithmetic with the *% operator (one mnemonic for remembering it is that this combines multiply (*) with modulus (%):

const std = @import("std");
pub fn main() void {
    @setRuntimeSafety(false);
    var x: u16 = 500;
    var y: u16 = 500;
    var z: u32 = x *% y;
    std.debug.print("{d}\n", .{z});
}

This will perform wrapping 16-bit arithmetic, and then coerce the result to 32 bits. In this case, 500*500 produces a 16-bit result of 1101000010010000 which is then extended to the 32-bit result 00000000000000001101000010010000, or 53392. Note that there is no way to tell if overflow occurred, since we explicitly asked for wrapping arithmetic.

2. Use @mulWithOverflow

const std = @import("std");
pub fn main() void {
    var x: u16 = 500;
    var y: u16 = 500;
    var z: u16 = undefined;
    if (@mulWithOverflow(u16, x, y, &z)) {
        std.debug.print("Oops! we had an overflow: {d}\n", .{z});        
    } else {
        std.debug.print("{d}\n", .{z});        
    }
}

This will perform a 16-bit multiply. If there is no overflow, it will return false and store the result in z. If there is an overflow, it will return true and store the overflowed bits in z. Output: Oops! we had an overflow: 53392.

3. Cast an operand to u32:

const std = @import("std");
pub fn main() void {
    var x: u16 = 500;
    var y: u16 = 500;
    var z: u32 = @as(u32, x) * y;
    std.debug.print("{d}\n", .{z});        
}

Casting an operand to u32 will cause Peer Type Resolution to resolve both arguments to u32, and produce a final result value of 250000

Reach out to learn more about Lager's hardware test automation platform.

Try One Month Free

hello@lagerdata.com