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
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
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]
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
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 bookto build the booknimble apidocsto build the API docsnimble docsto build both