Skip to main content
Version: dev

Integers

An integer type is a range constrained field type.

The Noir frontend supports both unsigned and signed integer types. The allowed sizes are 8, 16, 32, 64 and 128 bits. (For 128 bits, only unsigned is supported at the moment.)

info

When an integer is specified without a defined or inferred type, it defaults to Field. Except if they are one of the following, it defaults to u32 instead:

  • Array indices
  • Loop indices
  • Bitwise operations
  • Comparison operations
  • A modulo (%) operation
  • Constants in numeric generics

You can add a type suffix such as u32 or Field to the end of an integer literal to explicitly specify the type.

Integer Literal Syntax

Integer literals can include an optional type suffix to specify their type explicitly:

let a = 1u8;        // u8
let b = 256_u16; // u16
let c = -1i8; // i8
let d = 42_u32; // u32
let e = 100_Field; // Field

The supported suffixes are: u8, u16, u32, u64, u128, i8, i16, i32, i64, and Field.

Underscores can be used as visual separators in numeric literals for readability. They are ignored by the compiler:

let million = 1_000_000;
let max_u64 = 18_446_744_073_709_551_615_u64;

Unsigned Integers

An unsigned integer type is specified first with the letter u (indicating its unsigned nature) followed by its bit size (e.g. 8):

fn main() {
let x: u8 = 1;
let y = 1_u8;
let z = x + y;
assert (z == 2);
}

The bit size determines the maximum value the integer type can store. For example, a u8 variable can store a value in the range of 0 to 255 (i.e. 281\\2^{8}-1\\).

Signed Integers

A signed integer type is specified first with the letter i (which stands for integer) followed by its bit size (e.g. 8):

fn main() {
let x: i8 = -1;
let y = -1i8;
let z = x + y;
assert (z == -2);
}

The bit size determines the maximum and minimum range of value the integer type can store. For example, an i8 variable can store a value in the range of -128 to 127 (i.e. 27\\-2^{7}\\ to 271\\2^{7}-1\\).

fn main(x: i16, y: i16) {
// modulo
let c = x % y;
let c = x % -13;
}

Modulo operation is defined for negative integers thanks to integer division, so that the equality x = (x/y)*y + (x%y) holds.

Overflows

Computations that exceed the type boundaries will result in overflow errors. This happens with both signed and unsigned integers. For example, attempting to prove:

fn main(x: u8, y: u8) -> pub u8 {
let z = x + y;
z
}

With:

x = "255"
y = "1"

Would result in:

$ nargo execute
error: Assertion failed: 'attempt to add with overflow'
┌─ ~/src/main.nr:9:13

│ let z = x + y;
│ -----

= Call stack:
...

A similar error would happen with signed integers:

fn main() -> i8 {
let x: i8 = -118;
let y = -11;
let z = x + y;
z
}

Note that if a computation ends up being unused the compiler might remove it and it won't end up producing an overflow:

fn main() {
// "255 + 1" would overflow, but `z` is unused so no computation happens
let z: u8 = 255 + 1;
}

Wrapping methods

Although integer overflow is expected to error, some use-cases rely on wrapping. For these use-cases, the standard library provides wrapping variants of certain common operations via Wrapping traits in std::ops

fn wrapping_add(self, y: Self) -> Self;
fn wrapping_sub(self, y: Self) -> Self;
fn wrapping_mul(self, y: Self) -> Self;

Example of how it is used:

use std::ops::WrappingAdd;
fn main(x: u8, y: u8) -> pub u8 {
x.wrapping_add(y)
}