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).
-
(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:
For serialization (
toCell and similar), there is now one option:
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.