Integers
Arithmetic in smart contracts on TON is always done with integers and never with floating-point numbers since the floats are unpredictable. Therefore, the big accent goes on integers and their handling.
The only primitive number type in Tact is Int
, for -bit signed integers.
It’s capable of storing integers between and
Notation
Tact supports various ways of writing primitive values of Int
as integer literals.
Most of the notations allow adding underscores (_
) in-between digits, except for:
- Representations in strings, as seen in nano-tons case.
- Decimal numbers written with a leading zero Their use is generally discouraged, see below.
Additionally, several underscores in a row as in , or trailing underscores as in are not allowed.
Decimal
Most common and most used way of representing numbers, using the decimal numeral system:
You can use underscores (_
) to improve readability: is equal to
Hexadecimal
Represent numbers using hexadecimal numeral system, denoted by the (or ) prefix:
Use underscores (_
) to improve readability: is equal to
Octal
Represent numbers using octal numeral system, denoted by the (or ) prefix:
Use underscores (_
) to improve readability: is equal to
Binary
Represent numbers using binary numeral system, denoted by the (or ) prefix:
Use underscores (_
) to improve readability: is equal to
NanoToncoin
Arithmetic with dollars requires two decimal places after the dot — those are used for the cents value. But how would we represent the number $ if we’re only able to work with integers? The solution is to work with cents directly. This way, $ becomes cents. We simply memorize that the two rightmost digits represent the numbers after the decimal point.
Similarly, working with Toncoin, the main currency of TON Blockchain, requires nine decimal places instead of the two. One can say that nanoToncoin is the of the Toncoin.
Therefore, the amount of Toncoin, which can be represented in Tact as ton("1.25")
, is actually the number . We refer to such numbers as nanoToncoin(s) (or nano-ton(s)) rather than cents.
Serialization
When encoding Int
values to persistent state (fields of contracts and traits), it’s usually better to use smaller representations than -bits to reduce storage costs. Usage of such representations is also called “serialization” due to them representing the native TL-B types which TON Blockchain operates on.
The persistent state size is specified in every declaration of a state variable after the as
keyword:
contract SerializationExample { // persistent state variables oneByte: Int as int8 = 0; // ranges from -128 to 127 (takes 8 bit = 1 byte) twoBytes: Int as int16; // ranges from -32,768 to 32,767 (takes 16 bit = 2 bytes)
init() { // needs to be initialized in the init() because it doesn't have the default value self.twoBytes = 55*55; }}
Integer serialization is also available for the fields of Structs and Messages, as well as in key/value types of maps:
struct StSerialization { martin: Int as int8;}
message MsgSerialization { seamus: Int as int8; mcFly: map<Int as int8, Int as int8>;}
Motivation is very simple:
- Storing -bit integers in state costs about TON per year.
- Storing -bit integers only costs TON per year by comparison.
Common serialization types
Name | TL-B | Inclusive range | Space taken |
---|---|---|---|
uint8 | uint8 | to | bits = byte |
uint16 | uint16 | to | bits = bytes |
uint32 | uint32 | to | bits = bytes |
uint64 | uint64 | to | bits = bytes |
uint128 | uint128 | to | bits = bytes |
uint256 | uint256 | to | bits = bytes |
int8 | int8 | to | bits = byte |
int16 | int16 | to | bits = bytes |
int32 | int32 | to | bits = bytes |
int64 | int64 | to | bits = bytes |
int128 | int128 | to | bits = bytes |
int256 | int256 | to | bits = bytes |
int257 | int257 | to | bits = bytes + bit |
coins | VarUInteger 16 | to | between and bits, see below |
Arbitrary types of fixed bit-width
Available since Tact 1.5In addition to common serialization types, it’s possible to specify arbitrary bit-width integers by using a prefix int
or uint
followed by digits. For example, writing int7
refers to a signed -bit integer.
The minimum allowed bit-width of an Int
type is , while the maximum is for int
prefixes (signed integers) and for uint
prefixes (unsigned integers).
Name | TL-B | Inclusive range | Space taken |
---|---|---|---|
uintX | uintX | to | bits, where is between and |
intX | intX | to | bits, where is between and |
Types of variable bit-width
Name | TL-B | Inclusive range | Space taken |
---|---|---|---|
coins | VarUInteger 16 | to | between and bits |
In Tact, variable coins
format is an alias to VarUInteger 16
in TL-B representation, i.e. it takes a variable bit length depending on the optimal number of bytes needed to store the given integer and is commonly used for storing nanoToncoin amounts.
This serialization format consists of two TL-B fields:
len
, a -bit unsigned big-endian integer storing the byte length of the value providedvalue
, a -bit unsigned big-endian representation of the value provided
That is, integers serialized as coins
occupy between and bits ( bits for len
and to bytes for value
) and have values in the inclusive range from to .
Examples:
struct Scrooge { // len: 0000, 4 bits (always) // value: none! // in total: 4 bits a: Int as coins = 0; // 0000
// len: 0001, 4 bits // value: 00000001, 8 bits // in total: 12 bits b: Int as coins = 1; // 0001 00000001
// len: 0010, 4 bits // value: 00000001 00000010, 16 bits // in total: 20 bits c: Int as coins = 258; // 0010 00000001 00000010
// len: 1111, 4 bits // value: hundred twenty 1's in binary // in total: 124 bits d: Int as coins = pow(2, 120) - 1; // hundred twenty 1's in binary}
Name | TL-B | Inclusive range | Space taken |
---|---|---|---|
varuint16 | VarUInteger 16 | same as coins | same as coins |
varint16 | VarInteger 16 | to | between and bits |
varuint32 | VarUInteger 32 | to | between and bits |
varint32 | VarInteger 32 | to | between and bits |
The varuint16
format is equivalent to coins
. Its signed variant, varint16
, has the same memory layout except for the signed value
field, which allows a different range of values: from to , including both ends.
To store greater values, use varuint32
and varint32
formats. These are serialized almost identical to coins
and lesser variable integer formats, but use a -bit len
field for storing the byte length. This allows the value
to use up to bits for storing the actual number, meaning that both varuint32
and varint32
can occupy up to bits in total.
Examples:
struct BradBit { // len: 00000, 5 bits // value: none! // in total: 5 bits a: Int as varuint32 = 0; // 00000
// len: 00001, 5 bits // value: 00000001, 8 bits // in total: 13 bits b: Int as varuint32 = 1; // 00001 00000001
// len: 00010, 5 bits // value: 00000001 00000010, 16 bits // in total: 21 bits c: Int as varuint32 = 258; // 00010 00000001 00000010
// len: 11111, 5 bits // value: two hundred and forty-eight 1's in binary // in total: 253 bits d: Int as varuint32 = pow(2, 248) - 1; // two hundred and forty-eight 1's in binary}
Operations
All runtime calculations with numbers are done at 257-bits, so overflows are quite rare. Nevertheless, if any math operation overflows, an exception will be thrown, and the transaction will fail. You could say that Tact’s math is safe by default.
Note, that there is no problem with mixing variables of different state sizes in the same calculation. At runtime they are all the same type no matter what — -bit signed, so overflows won’t happen then.
However, this can still lead to errors in the compute phase of the transaction. Consider the following example:
import "@stdlib/deploy";
contract ComputeErrorsOhNo with Deployable { oneByte: Int as uint8; // persistent state variable, max value is 255
init() { self.oneByte = 255; // initial value is 255, everything fits }
receive("lets break it") { let tmp: Int = self.oneByte * 256; // no runtime overflow self.oneByte = tmp; // whoops, tmp value is out of expected range of oneByte }}
Here, oneByte
is serialized as a uint8
, which occupies only one byte and ranges from to , which is . And when used in runtime calculations no overflow happens and everything is calculated as a -bit signed integers. But the very moment we decide to store the value of tmp
back into oneByte
we get an error with the exit code 5, which states the following: Integer out of expected range
.