Introducing Achronyme — a language for zero-knowledge proofs. Read the announcement

BigInt Arithmetic

Fixed-width unsigned integers for cryptographic operations.

BigInt provides 256-bit and 512-bit unsigned integer arithmetic in the VM. Unlike Field elements (which use modular arithmetic over BN254), BigInts use non-modular arithmetic with explicit overflow/underflow errors. This makes them ideal for operations where you need exact integer semantics at cryptographic widths.

Creating BigInts

There are three ways to create BigInt values:

Literal syntax

Use the 0i prefix followed by width (256 or 512) and radix (x for hex, d for decimal, b for binary):

let a = 0i256xFF           // 256-bit, hex
let b = 0i256d255          // 256-bit, decimal
let c = 0i256b11111111     // 256-bit, binary
let d = 0i512x1234ABCD     // 512-bit, hex

All three representations of 255 are equal:

assert(0i256xFF == 0i256d255)
assert(0i256d255 == 0i256b11111111)

Constructor functions

bigint256() and bigint512() construct from integers or strings:

let a = bigint256(42)          // from integer
let b = bigint256("0xFF")     // from hex string
let c = bigint256("12345")    // from decimal string
let d = bigint512(0)           // 512-bit zero

Type checking

Use typeof() to inspect BigInt values:

let x = 0i256d42
print(typeof(x))   // "BigInt256"

let y = 0i512d42
print(typeof(y))   // "BigInt512"

Arithmetic

Standard operators work between BigInts of the same width:

let a = 0i256d100
let b = 0i256d200

print(a + b)   // BigInt256(0x12c)  (300)
print(b - a)   // BigInt256(0x64)   (100)
print(a * b)   // BigInt256(0x4e20) (20000)
print(b / a)   // BigInt256(0x2)    (2)
print(b % a)   // BigInt256(0x0)    (0)

Exponentiation uses ^ with an integer exponent:

let base = 0i256d2
let result = base ^ 128
print(result)  // 2^128 as a 256-bit integer

Overflow and Underflow

Unlike Field elements, BigInt arithmetic does not wrap around. Operations that exceed the range produce runtime errors:

// This will error: BigInt overflow
let max = bit_not(bigint256(0))  // all bits set = 2^256 - 1
let boom = max + bigint256(1)    // ERROR: BigIntOverflow
// This will error: BigInt underflow
let zero = bigint256(0)
let boom = zero - bigint256(1)   // ERROR: BigIntUnderflow

This makes bugs visible immediately rather than producing silently wrong results.

Type Safety

BigInt enforces strict type boundaries. You cannot mix BigInt with Int, Field, or a different BigInt width:

// All of these are runtime errors:
// bigint256(1) + 1           -- BigInt + Int
// bigint256(1) + 0p1         -- BigInt + Field
// bigint256(1) + bigint512(1) -- width mismatch

This prevents accidental arithmetic between incompatible types.

Bitwise Operations

BigInt supports full bitwise manipulation through native functions:

let a = 0i256xFF
let b = 0i256x0F

print(bit_and(a, b))   // 0x0F
print(bit_or(a, b))    // 0xFF
print(bit_xor(a, b))   // 0xF0
print(bit_not(b))      // all bits flipped

Shifts

let one = bigint256(1)

// Shift left: multiply by powers of 2
let shifted = bit_shl(one, 128)   // 2^128

// Shift right: divide by powers of 2
let back = bit_shr(shifted, 128)  // back to 1
assert(back == one)

Left shifts error if any set bits are shifted out (overflow protection). Right shifts discard shifted-out bits.

Bit Decomposition

Convert between BigInts and individual bits with to_bits() and from_bits():

let val = bigint256(42)
let bits = to_bits(val)

// bits is a list of 256 integers (0 or 1), LSB-first
print(len(bits))   // 256
print(bits[0])     // 0  (least significant bit)
print(bits[1])     // 1
print(bits[2])     // 0
print(bits[3])     // 1
print(bits[4])     // 0
print(bits[5])     // 1
// 42 = 0b101010, LSB-first = [0, 1, 0, 1, 0, 1, 0, 0, ...]

// Reconstruct from bits
let reconstructed = from_bits(bits, 256)
assert(reconstructed == val)

The second argument to from_bits() is the target width (256 or 512).

Example: Bit Counting

Count the number of set bits in a BigInt:

fn popcount(x) {
    let bits = to_bits(x)
    mut count = 0
    for b in bits {
        count = count + b
    }
    return count
}

assert(popcount(bigint256(0)) == 0)
assert(popcount(bigint256(1)) == 1)
assert(popcount(bigint256(255)) == 8)
assert(popcount(bigint256("0xFFFF")) == 16)

print("popcount(0xFF) =", popcount(bigint256(255)))  // 8

Example: XOR Swap

Swap two values using XOR without a temporary variable:

mut a = 0i256d42
mut b = 0i256d99

a = bit_xor(a, b)
b = bit_xor(b, a)
a = bit_xor(a, b)

assert(a == 0i256d99)
assert(b == 0i256d42)
print("After XOR swap: a =", a, "b =", b)

Example: Power of Two Check

Check whether a BigInt is a power of two using the bit trick n & (n - 1) == 0:

fn is_power_of_two(n) {
    if n == bigint256(0) {
        return false
    }
    let prev = n - bigint256(1)
    return bit_and(n, prev) == bigint256(0)
}

assert(is_power_of_two(bigint256(1)) == true)
assert(is_power_of_two(bigint256(256)) == true)
assert(is_power_of_two(bit_shl(bigint256(1), 128)) == true)
assert(is_power_of_two(bigint256(3)) == false)
assert(is_power_of_two(bigint256(100)) == false)

print("2^128 is power of 2:", is_power_of_two(bit_shl(bigint256(1), 128)))

BigInt vs Field

BigIntField
ArithmeticNon-modular (overflow errors)Modular (wraps around BN254 prime)
Widths256-bit, 512-bit254-bit (BN254 scalar field)
Circuit supportVM onlyBoth VM and circuits
Use caseExact integer arithmetic at crypto widthsZK circuit values, Poseidon hashing
DivisionInteger division (truncates)Modular inverse
NegationError (unsigned)Additive inverse in field

Choose BigInt when you need exact integer semantics. Choose Field when you need modular arithmetic or circuit compatibility.

Navigation