Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Quickstart

Installation

$ nimble install -y stint

Add Stint to your .nimble file:

requires "stint"

Usage

stint allows you to construct multi-precision integers from native integers, strings, and raw bytes and work with them the same way you would with Nim’s stock integers, i.e. use all the familiar arithmetic and bit operations (+, -, shl, shr, etc.).

Integers in stint are represented with StInt[bits] and StUint[bits] types for bits-sized signed and unsigned integers respectively.

There are many ways you can define an integer in stint. In this quickstart guide, we’ll list only the most common ones. You’ll find and exhaustive list in the Examples section.

128- and 256-bit Integers

Since 128 and 256-bit integers are the most commonly used bigints, stint provides convenience types and procs to define them.

i128, u128, i256, and u256 postfixes provide the most straightforward way of defining 128 and 256-bit integers:

import stint

# define signed and unsigned 128-bit integers:
let
  i0 = 123'i128
  u0 = 456'u128
  i1 = 321'i256
  u1 = 654'u256

echo i0, ": ", typeof(i0)
echo u0, ": ", typeof(u0)
echo i1, ": ", typeof(i1)
echo u1, ": ", typeof(u1)

# Output:
# 123: Int128
# 456: UInt128
# 321: Int256
# 654: UInt256

Note: Int128 and UInt128 are convenience types equivalent to StInt[128] and StUint[128] respectively. Similarly, Int256 and UInt256 are just StInt[256] and StUint[256].

To convert a native integer to Int128 or UInt128, use i128, u128, i256, and u256 as procs:

let
You   x = 123
  xi128 = i128(x)
  xu128 = u128(x)
  xi256 = i256(x)
  xu256 = u256(x)

echo xi128, ": ", typeof(xi128)
echo xu128, ": ", typeof(xu128)
echo xi256, ": ", typeof(xi256)
echo xu256, ": ", typeof(xu256)

# Output:
# 123: Int128
# 123: UInt128
# 123: Int256
# 123: UInt256

Arbitrary-sized Integers

stint lets you define integers of any size. The only requirement is that the size is known at compile time.

Use stint and stuint procs to convert a native integer to StInt[bits] and StUint[bits] respectively:

import stint

# define 231-bit integers:
let
  i2 = 1111.stint(231)
  u2 = 2222.stuint(231)

echo i2, ": ", typeof(i2)
echo u2, ": ", typeof(u2)

# Output:
# 1111: StInt[231]
# 2222: StUint[231]

Arithmetic Operations

If you have two integers of the same type, you can do pretty much everything with them that you can with Nim’s standard integers.

Here are some examples of the arithmetic operators provided by stint:

import stint

let
  si0 = 123'i128 + 456'i128
  su0 = 456'u128 + 789'u128
  di0 = 17541'i128 div 13'i128
  du0 = 17541'u128 div 13'u128

echo si0, ": ", typeof(si0)
echo su0, ": ", typeof(su0)
echo di0, ": ", typeof(di0)
echo du0, ": ", typeof(du0)

# Output:
# 579: Int128
# 1245: UInt128
# 1349: Int128
# 1349: StUint[128]
```

Note that you can't run operations on stint integers of different size:
```nim
# This won't compile:
let se = 123'i128 + 456'i256

However, you can combine stint integers with native ones in some operations:

let
  si1 = 123'i128 + 100'u64
  su1 = 456'u128 + 100
  du1 = 17541'u128 div 13

echo si1, ": ", typeof(si1)
echo su1, ": ", typeof(su1)
echo 456'u128 + 100 == 456'u128 + 100'u64
echo du1, ": ", typeof(du1)

# Output:
# 223: Int128
# 223: StUint[128]
# true
# 1349: StUint[128]

You’ll find more usage examples in the Examples section.

For a full list of available operations, refer to the API index.

Bitwise Operations

Here are some examples of the bitwise operators provided by stint:

import stint

let bi0 = 123'i128

echo bi0 shl 1
echo bi0.getBit(3)
echo bi0 and 456'i128

# Output:
# 246
# true
# 72

You’ll find more usage examples in the Examples section.

For a full list of available operations, refer to the API index.

Examples

Defining Integers

i128, u128, i256, u256

i128, u128, i256, and u256 procs convert a native integer or a string into a stint integer:

import stint

let
  i = 123
  s = "456"
  x = i128(i)
  y = i128(s)

echo x
echo y

# Output:
# 123
# 456

Use these functions in postfix notation to define stint integers from integer literals:

echo 123'i128
echo typeof(123'i128)

# Output:
# 123
# Int128

API reference

stint and stuint

stint and stuint procs are used to convert native integers to multi-precision integers of arbitrary size:

import stint

echo 123.stuint(256)
echo typeof(123.stuint(256))
echo 456.stint(300)
echo typeof(123.stint(300))

# Output:
# 123
# StUint[256]echo 123.stuint(256)
echo typeof(123.stuint(256))
echo 456.stint(300)
echo typeof(123.stint(300))
# 456
# StInt[300]

API reference:

parse

parse proc is used to convert a string to multi-precision integer:

import stint

echo "123".parse(Int128)
echo typeof("123".parse(Int128))
echo "456".parse(StUint[256])
echo typeof("123".parse(StUint[256]))

# Output:
# 123
# Int128
# 456
# UInt256

API reference

fromHex, fromDecimal, hexToUint

fromHex and fromDecimal procs are syntactic sugar for the parse proc for specific cases when you’re parsing a string representing an integer in hexadecimal or decimal format.

hexToUint is an even more specific sugar for parsing a hex-string to a StUint:

import stint

echo UInt128.fromHex("0x123123")
echo Int128.fromDecimal("123123")
echo hexToUint[128]("0x123123")

# Output:
# 1192227
# 123123
# 1192227

API reference:

to

to is syntactic sugar for stint and stuint procs:

import stint

echo 123.to(UInt256)
echo typeof(123.to(UInt256))
echo 456.to(StInt[300])
echo typeof(456.to(StInt[300]))

# Output:
# 123
# UInt256
# 456
# StInt[300]

API reference

readIntBE, readIntLE, readUintBE, readUintLE

These functions allow you create multi-precition integers from arrays of bytes. BE and LE stand for “big-endian” and “little-endian”, i.e. determine the direction of the byte array parsing (left to right or right to left):

import stint

echo readIntBE[128]([byte 1, 2, 3])
echo readIntLE[128]([byte 1, 2, 3])

echo readUintBE[128]([byte 1, 2, 3])
echo readUintLE[128]([byte 1, 2, 3])

# Output:
66051
197121
66051
197121

API reference

Serializing Integers

toString, $, toHex

toString returns a string representation of a given integer in the given radix. $ and toHex are just toString with radix 10 and 16 respectively:

import stint

echo 123'u128.toString(10)
echo 123'u128.toString(16)
echo $123'u128
echo 123'u128.toHex()

# Output:
# 123
# 7b
# 123
# 7b

API reference:

Operations

Arithmeric Operations on the Same Type

import stint

echo 456'u128 + 123'u128
echo 456'u128 - 123'u128
echo 456'u128 * 123'u128
echo 456'u128 div 123'u128
echo 456'u128 mod 123'u128
echo divmod(456'u128, 123'u128 )

# Output:
# 579
# 333
# 56088
# 3
# 87
# (quot: 3, rem: 87)

In-place Updates

var x = 123'i128

x += 100'i128
echo x
x -= 50'i128
echo x

# Output:
# 223
# 173

Arithmetic Operations with Mixed Types

echo 100'u128.pow(3)
echo 456'u128 + 100
echo 456'i128 + 100'u64

# Output:
# 1000000
# 556
# 556

Bitwise Operations

echo 123'u128 shl 1
echo 123'u128 shr 1
echo 123'u128 or 456'u128
echo 123'u128 and 456'u128
echo 123'u128 xor 456'u128

echo 123'i128 shl 1
echo 123'i128 shr 1
echo 123'i128 or 456'i128
echo 123'i128 and 456'i128
echo 123'i128 xor 456'i128

# Output:
# 246
# 61
# 507
# 72
# 435
# 246
# 61
# 507
# 72
# 435

API reference:

Modular Arithmetic

import stint

echo addmod(456'u128, 123'u128, 7'u128)
echo submod(456'u128, 123'u128, 7'u128)
echo mulmod(456'u128, 123'u128, 7'u128)
echo powmod(456'u128, 123'u128, 7'u128)

echo addmod(456'i128, 123'i128, 7'i128)
echo submod(456'i128, 123'i128, 7'i128)
echo mulmod(456'i128, 123'i128, 7'i128)
echo powmod(456'i128, 123'i128, 7'i128)

# Output:
# 5
# 4
# 4
# 1
# 5
# 4
# 4
# 1

API reference:

Usage Examples

addmul

import stint

func addmul(a, b, c: UInt256): UInt256 =
  a * b + c

echo addmul(u256"100000000000000000000000000000", u256"1", u256"2")

# Output:
# 100000000000000000000000000002

Crypto: Check User Balance

Checking if a user has enough balance for a transaction including gas fees:

import stint

let
  senderBalance = u256"100000000000000000000"
  transferAmount = u256"5000000000000000000"
  gasPrice = u256"20000000000"
  gasLimit = u256"21000"

let totalCost = transferAmount + (gasPrice * gasLimit)

if senderBalance >= totalCost:
  let newBalance = senderBalance - totalCost
  echo "Transfer successful. Remaining balance: ", newBalance
else:
  echo "Insufficient funds."

# Output:
# Transfer successful. Remaining balance: 94999580000000000000

Modular Arithmetic (Diffie-Hellman Key Exchange)

import stint

let p = UInt256.fromHex("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F")
let g = u256"2"
let privateKey = UInt256.fromHex("0x4a1b0c8e1d2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f")

let publicKey = powmod(g, privateKey, p)

echo "Public Key to share: ", publicKey.toHex()

# Output:
# Public Key to share: 49a096670adb4db966020006267c7db8179ef7d776cb739fe3640366343ace73

Zero Memory Overhead Raw Byte Parsing

Parsing a network packet header or binary file where a 128-bit ID is stored as big-endian bytes:

import stint

let rawBytes = [
  0x00'u8, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D,
  0x0E, 0x0F,
]

let packetId = readUintBE[128](rawBytes)

let mask = UInt128.fromHex("0xFFFF0000000000000000000000000000")

if (packetId and mask) != 0.u128:
  echo "Header flag is set!"

echo "Packet ID: ", packetId

# Output:
# Header flag is set!
# Packet ID: 5233100606242806050955395731361295

Large Number Computation

Use 512-bit integers to calculate 50! (regular Nim uint64 would have overflown):

import stint

type UInt512 = Stint[512]

func factorial(n: int): UInt512 =
  result = 1.to(UInt512)
  for i in 1..n:
    result = result * i.to(UInt512)

let fact50 = factorial(50)

echo "50! = ", fact50

# Output:
# 50! = 30414093201713378043612608166064768844377641568960512000000000000

Contributor’s Guide

Multi-Precision Integers 101

Native Nim integers have a fixed maximal size: 32 bits for int32 and uint32, 64 bits for int64 and uint64. This means that you can’t store a value that won’t fit this size.

However, many real-life tasks require the ability to work with integers of arbitrary size regardless of the technical limitations. Integers that aren’t bound to a particluar precision level are called multi-precision.

Multi-precision integers are represented in computer programs as sequences of limbs, each one representing a singe word of fixed size. Depending on the CPU architecture, a word can be 32 or 64 bits of size.

Limbs are like digits in ordinary base-10 numbers but each “digit” can have not 10 possible values but 2^32 or 2^64 possible values.

Doing arithmetic with multi-precision integers means doing arithmetic with each limb separately and carrying the result from limb to limb, similarly how you did addition at school.

stint uses arithmetic operations on individual words from intops and implements the operations on the whole multi-precision integers.

Library Structure

[Repository Root]
│   stint.nim                       <- API entrypoint;
│                                      users normally import just that
│
├───benchmarks
│       bench.nim                   <- Benchmark runner
│
├───helpers
│       prng_unsafe.nim             <- Pseudo-random number generator,
│                                      used in tests and benchmarks
├───stint
│   │   endians2.nim                <- Endianness utilities: byte swapping,
│   │                                  byte array <-> integer conversions 
│   │ 
│   │   intops.nim                  <- Arithmetic operations for signed integers
│   │ 
│   │   int_modarith.nim            <- Modular arithmetic for signed integers
│   │ 
│   │   io.nim                      <- String, byte, and native int parsing,
│   │                                  hex conversion, and string formatting
│   │ 
│   │   lenient_stint.nim           <- [Marked for deprecataion]
│   │                                  Sugar for mixed-precision integer operations
│   │ 
│   │   literals_stint.nim          <- Defines literal macros (u256, u128, etc.)
│   │                                  for easy constant creation
│   │ 
│   │   modular_arithmetic.nim      <- Modular arithmetic for unsigned integers
│   │ 
│   │   uintops.nim                 <- Arithmetic operations for unsigned integers
│   │
│   └───private
│           custom_literal.nim      <- Helper procs for string parsing used in io.nim
│           datatypes.nim           <- stint's types: StInt, Int128, Word, etc.
│           uint_addsub.nim         <- Unsigned integer operation implementations.
│           ...
│
└───tests
    │   all_tests.nim               <- Tests for the publically exposed procs
    │   internal.nim                <- Tests for the private procs
    │   internal_uint_div.nim       <- Dedicated tests for the uint_div procs
    │   test_bugfix.nim             <- Test suites executed with all_tests.nim 
    │   ...

Tests

Run tests of the private procs:

$ nimble test_internal

Test the publically facing procs:

$ nimble test_public_api

Run all tests:

$ nimble test

Benchmarks

To run the benchmarks, compile and run benchmarks/bench.nim with maximum optimizations:

$ nim r -d:danger --passC:"-march=native -O3" benchmarks/bench.nim

Docs

The docs consist of two parts:

  • the book (this is what you’re reading right now)
  • the API docs

The book is created using mdBook.

The API docs are generated from the source code docstrings.

To build the docs locally, run:

  • nimble book to build the book
  • nimble apidocs to build the API docs
  • nimble docs to build both