Preventing integer overflow in Zig

Consider the following Zig program:

{% c-block language="zig" %}
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});
}
{% c-block-end %}

Run it with {% c-line %}zig run overflow.zig{% c-line-end %}:

{% c-block language="console" %}
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
{% c-block-end %}

{% c-line %}500 * 500{% c-line-end %} is only {% c-line %}250,000{% c-line-end %} - so what happened? In Zig, the multiplication operator invokes Peer Type Resolution on its operands. In this case, the operands are both {%c-line %}u16{% c-line-end%}, so a 16-bit multiply will be performed, resulting in a value of {% c-line %}250000{% c-line-end%}, which is larger than the maximum {% c-line %}u16{% c-line-end%} value of {% c-line %}65535{% c-line-end%} - 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 {% c-line %}*%{% c-line-end %} operator (one mnemonic for remembering it is that this combines multiply ({% c-line %}*{% c-line-end %}) with modulus ({% c-line %}%{% c-line-end %}):

{% c-block language="zig" %}
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});
}
{% c-block-end %}

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

2. {% c-line%}Use @mulWithOverflow{% c-line-end %}

{% c-block language="zig" %}
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});        
   }
}
{% c-block-end %}

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

3. Cast an operand to {% c-line %}u32{% c-line-end %}:

{% c-block language="zig" %}
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});        
}
{% c-block-end %}

Casting an operand to {% c-line %}u32{% c-line-end%} will cause Peer Type Resolution to resolve both arguments to {% c-line %}u32{% c-line-end %}, and produce a final result value of {% c-line %}250000{% c-line-end %}

Want to stay up to date on the future of firmware? Join our mailing list.

Section
Chapter
Published