Key features of auto-serialization
- Supports all types: unions, tensors, nullables, generics, atomics, …
- Allows you to specify serialization prefixes (particularly, opcodes)
- Allows you to manage cell references and when to load them
- Lets you control error codes and other behavior
- Unpacks data from a cell or a slice, mutate it or not
- Packs data to a cell or a builder
- Warns if data potentially exceeds 1023 bits
- More efficient than manual serialization
List of supported types and how they are serialized
A small reminder: Tolk hasintN types (int8, uint64, etc.). Of course, they can be nested, like nullable int32? or a tensor (uint5, int128).
They are just integers at the TVM level, they can hold any value at runtime: overflow only happens at serialization.
For example, if you assign 256 to uint8, asm command “8 STU” will fail with code 5 (integer out of range).
| Type | TL-B equivalent | Serialization notes |
|---|---|---|
int8, uint55, etc. | same as TL-B | N STI / N STU |
coins | VarUInteger 16 | STGRAMS |
varint16, 32, etc. | Var(U)Integer 16/32 | STVARINT16 / STVARUINT32 / etc. |
bits8, bits123, etc. | just N bits | runtime check + STSLICE (1) |
address | addr_std (internal, 267 bits) | STSTDADDR |
address? | addr_none or addr_std (2/267 bits) | STOPTSTDADDR |
any_address | MsgAddress (internal/external/none) | STSLICE |
bool | one bit | 1 STI |
cell | untyped reference, TL-B ^Cell | STREF |
cell? | maybe reference, TL-B (Maybe ^Cell) | STOPTREF |
Cell<T> | typed reference, TL-B ^T | STREF |
Cell<T>? | maybe typed reference, TL-B (Maybe ^T) | STOPTREF |
RemainingBitsAndRefs | rest of slice | STSLICE |
builder | only for writing, not for reading (4) | STB |
slice | only for writing, not for reading (4) | STSLICE |
T? | TL-B (Maybe T) | 1 STI + IF … |
T1 | T2 | TL-B (Either T1 T2) | 1 STI + IF … + ELSE … (2) |
T1 | T2 | ... | TL-B multiple constructors | IF … + ELSE IF … + ELSE … (3) |
(T1, T2) | TL-B (Pair T1 T2) = one by one | pack T1 + pack T2 |
(T1, T2, ...) | nested pairs = one by one | pack T1 + pack T2 + … |
SomeStruct | fields one by one | like a tensor |
enum SomeEnum | intN or uintN, where N manual or auto | enum SomeEnum: int8 for manual |
-
(1) By analogy with
intN, there arebytesNtypes. It’s just asliceunder the hood: the type shows how to serialize this slice. By default, beforeSTSLICE, the compiler inserts runtime checks (get bits/refs count + compare with N + compare with 0). These checks ensure that serialized binary data will be correct, but they cost gas. However, if you guarantee that a slice is valid (for example, it comes from trusted sources), pass an optionskipBitsNValidationto disable runtime checks. -
(2) TL-B Either is expressed with a union
T1 | T2. For example,int32 | int64is packed as (“0” + 32-bit int OR “1” + 64-bit int). However, if T1 and T2 are both structures with manual serialization prefixes, those prefixes are used instead of a 0/1 bit. -
(3) To pack or unpack a union, say,
Msg1 | Msg2 | Msg3, we need serialization prefixes. For structures, you can specify them manually (or the compiler will generate them right here). For primitives, likeint32 | int64 | int128 | int256, the compiler generates a prefix tree (00/01/10/11 in this case). Read auto-generating serialization prefixes below. -
(4) Using raw
builderandslicefor writing is supported but not recommended, as they do not reveal any information about their internal structure. Auto-generated TypeScript wrappers will be available in the future. However, for a rawslice, a wrapper cannot be generated, as there is no way to predict how to read such a field back. This feature will likely be removed in the future.
Some examples of valid types
Serialization prefixes and opcodes
Declaring a struct, there is a special syntax to provide pack prefixes. Typically, you’ll use 32-bit prefixes for messages opcodes, or arbitrary prefixes is case you’d like to express TL-B multiple constructors.0x000F— 16-bit prefix0x0F— 8-bit prefix0b010— 3-bit prefix0b00001111— 8-bit prefix
Asset will follow manually provided prefixes:
What can NOT be serialized
intcan’t be serialized, it does not define binary width; useint32,uint64, etc.slice, for the same reason; useaddressorbitsN- tuples, not implemented
A | B(andA|B|C|...in general) if A has manual serialization prefix, B not (because it seems like a bug in your code)int32 | A(andprimitives|...|structsin general) if A has manual serialization prefix (because it’s not definite what prefixes to use for primitives)
Error messages if serialization unavailable
If you, by mistake, use unsupported types, Tolk compiler will fire a meaningful error. Example:Controlling cell references. Typed cells
Tolk gives you full control over how your data is placed in cells and how cells reference each other. When you declare fields in a struct, there is no compiler magic of reordering fields, making any implicit references, etc. As follows, whenever you need to place data in a ref, you do it manually. As well as you manually control, when contents of that ref is loaded. There are two types of references: typed and untyped.NftCollectionStorage.fromSlice (or fromCell), the process is as follows:
- read address (slice.loadAddress)
- read uint64 (slice.loadUint 64)
- read three refs (slice.loadRef); do not unpack them: we just have pointers to cells
royaltyParams is Cell<T>, not T itself. You can NOT access numerator, etc. To load those fields, you should manually unpack that ref:
T.toCell() makes Cell<T>, actually. That’s true:
Cell<address> or even Cell<int32 | int64> is also okay, you are not restricted to structures.
When it comes to untyped cells — just cell — they also denote references, but don’t denote their inner contents, don’t have the .load() method.
It’s just some cell, like code/data of a contract or an untyped nft content.
Remaining data after reading
Suppose you havestruct Point (x int8, y int8), and read from a slice with contents 0102FF. Byte 01 for x, byte 02 for y, and the remaining FF — is it correct?
By default, this is incorrect. By default, functions fromCell and fromSlice ensure the slice end after reading.
In this case, exception 9 (“cell underflow”) is thrown.
But you can override this behavior with an option:
Custom serializers for custom types
Imagine that you have your own “variable-length string”: you encode its len, and then data, likeVariadicString as a regular type — everywhere:
packToBuilder and unpackFromSlice are reserved for this purpose, their prototype must be exactly as showed.
UnpackOptions and PackOptions
They allow you to control behavior offromCell, toCell, and similar functions:
fromCell and similar), there are now two available options:
| Field of UnpackOptions | Description |
|---|---|
assertEndAfterReading | after finished reading all fields from a cell/slice, call slice.assertEnd to ensure no remaining data left; it’s the default behavior, it ensures that you’ve fully described data you’re reading with a struct; for struct Point, input 0102 is ok, 0102FF will throw excno 9; default: true |
throwIfOpcodeDoesNotMatch | this excNo is thrown if opcode doesn’t match, e.g. for struct (0x01) A given input “88…”; similarly, for a union type, this is thrown when none of the opcodes match; default: 63 |
toCell and similar), there is now one option:
| Field of PackOptions | Description |
|---|---|
skipBitsNValidation | when a struct has a field of type bits128 and similar (it’s a slice under the hood), by default, compiler inserts runtime checks (get bits/refs count + compare with 128 + compare with 0); these checks ensure that serialized binary data will be correct, but they cost gas; however, if you guarantee that a slice is valid (for example, it comes from trusted sources), set this option to true to disable runtime checks; note: int32 and other are always validated for overflow without any extra gas, so this flag controls only rarely used bitsN types; default: false |
Full list of serialization functions
Each of them can be controlled byPackOptions described above.
T.toCell()— convert anything to a cell. Example:
builder.storeAny<T>(v)— similar tobuilder.storeUint()and others, but allows storing structures. Example:
Full list of deserialization functions
Each of them can be controlled byUnpackOptions described above.
T.fromCell(c)— parse anything from a cell. Example:
T.fromSlice(s)— parse anything from a slice. Example:
excode 9 “cell underflow”). Note, that a passed slice is NOT mutated; its internal pointer is NOT shifted. If you need to mutate it, like cs.loadInt(), consider calling cs.loadAny<Increment>().
slice.loadAny<T>— parse anything from a slice, shifting its internal pointer. Similar toslice.loadUint()and others, but allows loading structures. Example:
MyStorage.fromSlice(cs), but called as a method and mutates the slice. Note: options.assertEndAfterReading is ignored by this function, because it’s actually intended to read data from the middle.
slice.skipAny<T>— skip anything in a slice, shifting its internal pointer. Similar toslice.skipBits()and others, but allows skipping structures. Example:
Special type RemainingBitsAndRefs
It’s a built-in type to get “all the rest” slice tail on reading. Example:Auto-generating prefixes for unions
We’ve mentioned multiple times, thatT1 | T2 is encoded as TL-B Either: bit ‘0’ + T1 OR bit ‘1’ + T2. But what about wider unions? Say,
- if
nullexists, it’s 0, all others are 1+tree: A|B|C|D|null => 0 | 100+A | 101+B | 110+C | 111+D - if no
null, just distributed sequentially: A|B|C => 00+A | 01+B | 10+C
- if you specify prefixes manually, they will be used (no matter within a union or not)
- if you don’t specify any prefixes, the compiler auto-generates a prefix tree
- if you specify prefix for A, but forgot prefix for B,
A | Bcan’t be serialized - either bit (0/1) is just a prefix tree for two cases
Prefixed<int32, 0b0011>.
But you can just create a struct with a single field:
int32, the same slot on a stack, just adding a prefix for (de)serialization.
What if data exceeds 1023 bits
Struct fields are serialized one-by-one. So, if you have a large structure, its content may not fit into a cell. Tolk compiler calculates the maximum size of every serialized struct, and if it potentially exceeds 1023 bits, fires an error. Your choice is- either to suppress the error by placing an annotation above a struct; it means “okay, I understand”
- or repack your data by splitting into multiple cells
int8?is either one or nine bitscoinsis variadic: from 4 bits (small values) up to 124 bitsany_addressis internal (267 bits), or external (up to 521 bits), or none (2 bits)
- you definitely know, that
coinsfields will be relatively small, and this struct will 100% fit in reality; then, suppress the error using an annotation:
- or you really expect billions of billions in
coins, so data really can exceed; in this case, you should extract some fields into a separate cell; for example, store 800 bits as a ref; or extract other 2 fields and ref them:
Integration with message sending
Auto-serialization is natively integrated with sending messages to other contracts. You just “send some object,” and the compiler automatically serializes it, detects whether it fits into a message cell, etc.Not “fromCell” but “lazy fromCell”
Tolk has a special keywordlazy that is combined with auto-deserialization. The compiler will load not a whole struct, but only fields requested.