TL-B language
TL-B (Type Language - Binary) describes the type system, constructors, and available functions. For example, TL-B schemes can be used to define binary structures associated with the TON blockchain. Special TL-B parsers can read these schemes to deserialize binary data into various objects. TL-B specifically describes data schemes for cell
objects. If you are unfamiliar with cells
, please refer to the Cell & Bag of Cells (BoC) article.
Overview
A set of TL-B constructs is referred to as a TL-B document. A typical TL-B document consists of type declarations, i.e., their constructors, and functional combinators. Each combinator declaration ends with a semicolon ;
.
Here is an example of a combinator declaration:


Constructors
The left-hand side of each equation defines how to construct or serialize a value of the type specified on the right-hand side. Each description begins with the name of a constructor.


Constructors define a combinator's type, including its state during serialization. For example, constructors can specify an op
(operation code) in a query sent to a smart contract in TON.
// ....
transfer#5fcc3d14 <...> = InternalMsgBody;
// ....
- Constructor name:
transfer
- Constructor prefix code:
#5fcc3d14
Note that every constructor name is immediately followed by an optional constructor tag, such as #_
or $10
, which specifies the bitstring used to encode (serialize) the corresponding constructor.
message#3f5476ca value:# = CoolMessage;
bool_true$0 = Bool;
bool_false$1 = Bool;
The left-hand side of each equation defines how to construct or serialize a value of the type specified on the right-hand side.
It begins with the name of a constructor, such as a message
or bool_true
, followed by an optional constructor tag, such as #3f5476ca
or $0
, which specifies the bitstring used to encode (serialize) the constructor.
Constructor | Serialization |
---|---|
some#3f5476ca | A 32-bit uint is serialized from a hex value. |
some#5fe | A 12-bit uint is serialized from a hex value. |
some$0101 | Serialize the 0101 raw bits. |
some or some# | Serialize crc32(equation) | 0x80000000 . |
some#_ or some$_ or _ | Serialize nothing. |
Constructor names, such as some
in this example, are used as variables during code generation. For example:
bool_true$1 = Bool;
bool_false$0 = Bool;
The type Bool
has two tags: 0
and 1
. The code generation pseudocode might look like this:
class Bool:
tags = [1, 0]
tags_names = ['bool_true', 'bool_false']
If you do not want to assign a name to the current constructor, you can use _
. For example: _ a:(## 32) = 32Int;
Constructor tags can be specified in binary (prefixed with a dollar sign) or hexadecimal (prefixed with a hash sign). If a tag is not explicitly provided, the TL-B parser will compute a default 32-bit constructor tag by applying the CRC32 algorithm to the text of the constructor "equation" with | 0x80000000
. Therefore, if an empty tag is intended, it must be explicitly defined using #_
or $_
.
This tag is used during the deserialization process to determine the type of the bitstring. For example, if we have a 1-bit bitstring 0
and specify that it should be parsed as type Bool
, the TL-B parser will interpret it as Bool.bool_false
.
Let's now look at some more complex examples:
tag_a$10 val:(## 32) = A;
tag_b$00 val(## 64) = A;
When parsing the binary string 1000000000000000000000000000000001
—a 1
followed by 32 zeros and ending with another 1
—using TLB type A
, we first extract the initial two bits to determine the tag.
In this case, the first two bits are 10
, which correspond to tag_a
.
After identifying the tag, the next 32 bits are interpreted as the val
variable. In this example, the value is 1
.
The parsed pseudocode representation is as follows:
A.tag = 'tag_a'
A.tag_bits = '10'
A.val = 1
All constructor names must be distinct, and constructor tags within the same type must form a prefix code to ensure unambiguous deserialization. In other words, no tag can be a prefix of another tag within the same type.
- Maximum number of constructors per type:
64
. - Maximum number of bits for a tag:
63
.
example_a$10 = A;
example_b$01 = A;
example_c$11 = A;
example_d$00 = A;
The code generation pseudocode might look as follows:
class A:
tags = [2, 1, 3, 0]
tags_names = ['example_a', 'example_b', 'example_c', 'example_d']
example_a#0 = A;
example_b#1 = A;
example_c#f = A;
The code generation pseudocode might look as follows:
class A:
tags = [0, 1, 15]
tags_names = ['example_a', 'example_b', 'example_c']
If the hex
tag is used, remember that it is serialized as 4 bits per hexadecimal character. The maximum supported value is a 63-bit unsigned integer. This implies the following:
a#32 a:(## 32) = AMultiTagInt;
b#1111 a:(## 32) = AMultiTagInt;
c#5FE a:(## 32) = AMultiTagInt;
d#3F5476CA a:(## 32) = AMultiTagInt;
Constructor | Serialization |
---|---|
a#32 | A 8-bit uint is serialized from a hex value. |
b#1111 | A 16-bit uint is serialized from a hex value. |
c#5FE | A 12-bit uint is serialized from a hex value. |
d#3F5476CA | A 32-bit uint is serialized from a hex value. |
Hex values are allowed in both uppercase and lowercase formats.
More about the hex tags
In addition to the standard hex tag definition, a hexadecimal number may be followed by an underscore _
character.
This indicates that the tag should be interpreted as the hexadecimal value with the least significant bit (LSB) removed.
For example, consider the following schema:
vm_stk_int#0201_ value:int257 = VmStackValue;
In this case, the tag is not equal to 0x0201
. To compute the actual tag, remove the LSB from the binary representation of 0x0201
:
0000001000000001 -> 000000100000000
The resulting tag is the 15-bit binary number 0b000000100000000
.
Field definitions
Field definitions follow each constructor and its optional tag. A field definition has the format ident:type-expr
, where:
ident
is the field's name (use_
for anonymous fields).type-expr
is the field's type.
The type-expr
can be a simple type, a parameterized type with appropriate arguments, or a more complex expression.
Note: the total size of all fields in a type must not exceed the limits of a single cell — 1023
bits and 4
references.
Simple types
_ a:# = Type;
-Type.a
is a 32-bit integer._ a:(## 64) = Type;
-Type.a
is a 64-bit integer._ a:Owner = NFT;
-NFT.a
is of typeOwner
._ a:^Owner = NFT;
-NFT.a
is a cell ref toOwner
type - i.e., theOwner
is stored in the next referenced cell.
Anonymous fields
_ _:# = A;
- the first field is an anonymous 32-bit integer.
Extend cell with references
_ a:(##32) ^[ b:(## 32) c:(## 32) d:(## 32)] = A;
- If needed, specific fields can be stored in a separate cell using the
^[ ... ]
syntax. For example, in the following structure,A.a
/A.b
/A.c
/A.d
are all 32-bit unsigned integers. However,A.a
is stored in the primary cell, whileA.b
/A.c
/A.d
are stored in a referenced cell using 1 cell reference:
_ ^[ a:(## 32) ^[ b:(## 32) ^[ c:(## 32) ] ] ] = A;
- Chains of references are also allowed. In the following example, each variable (
a
,b
,c
) is stored in a separate cell, resulting in a chain of three referenced cells:
Parametrized types
Suppose we have the IntWithObj
type defined as follows:
_ {X:Type} a:# b:X = IntWithObj X;
Now, we can use this type in other types, as shown in the following examples:
_ a:(IntWithObj uint32) = IntWithUint32;
Complex expressions
-
Conditional fields (only for
Nat
)The expression
E?T
means that if the conditionE
is true, then the field has the typeT
.Example
_ a:(## 1) b:a?(## 32) = Example;
In
Example
type, the fieldb
is serialized only ifa
is equal to1
. -
Multiplicative expression for tuple creation
The expression
x * T
creates a tuple of lengthx
, where each element is of typeT
.Example
a$_ a:(## 32) = A;
b$_ b:(2 * A) = B;_ (## 1) = Bit;
_ 2bits:(2 * Bit) = 2Bits; -
Bit selection (only for
Nat
)The expression
E . B
means to take bitB
from theNat
valueE
.Example
_ a:(## 2) b:(a . 1)?(## 32) = Example;
In
Example
type, the variableb
is serialized only if the second bit ofa
is1
. -
Other
Nat
operatorsOther operations on
Nat
types are also supported (refer toAllowed constraints
).Note: you can combine multiple complex expressions:
Example
_ a:(## 1) b:(## 1) c:(## 2) d:(a?(b?((c . 1)?(## 64)))) = A;
Built-in types
#
-Nat
: 32-bit unsigned integer.## x
-Nat
: unsigned integer withx
bits.#< x
-Nat
: unsigned integer less thanx
bits, stored aslenBits(x - 1)
bits up to 31 bits.#<= x
-Nat
: unsigned integer less than or equal tox
bits, stored aslenBits(x)
bits up to 32 bits.Any
/Cell
- remaining bits and references.Int
- 257 bitsUInt
- 256 bitsBits
- 1023 bitsuint1
-uint256
- 1 - 256 bitsint1
-int257
- 1 - 257 bitsbits1
-bits1023
- 1 - 1023 bitsuint X
/int X
/bits X
- same asuintX
but can use a parametrizedX
in this types
Constraints
_ flags:(## 10) { flags <= 100 } = Flag;
Nat
fields are allowed in constraints. For example, the constraint { flags <= 100 }
means that the flags
variable is less than or equal to 100
.
Allowed contraints: E
| E = E
| E <= E
| E < E
| E >= E
| E > E
| E + E
| E * E
| E ? E
Implicit fields
Some fields may be implicit. These fields are defined within curly brackets ({
, }
), indicating that they are not directly serialized. Instead, their values must be deduced from other data, usually the parameters of the type being serialized.
Example
nothing$0 {X:Type} = Maybe X;
just$1 {X:Type} value:X = Maybe X;
_ {x:#} a:(## 32) { ~x = a + 1 } = Example;
Parametrized types
Variables — the identifiers of previously defined fields of types #
(natural numbers) or Type
(types of types) — may be used as parameters for parametrized types. During serialization, each field is recursively serialized according to its type. The final serialized value is the concatenation of bits representing the constructor, i.e., the constructor tag and the serialized field values.
Natural numbers (Nat
)
_ {x:#} my_val:(## x) = A x;
This means that A
is parametrized by x
, where x
is a Nat
. During the deserialization process, we will fetch an x
-bit unsigned integer. For example:
_ value:(A 32) = My32UintValue;
This means that during the deserialization process of the My32UintValue
type, we will fetch a 32-bit unsigned integer, as specified by the 32
parameters in the A
-type.
Types
_ {X:Type} my_val:(## 32) next_val:X = A X;
This means that the X
type parametrizes A
. During the deserialization process, we will first fetch a 32-bit unsigned integer and then parse the bits and references of the X
type.
An example usage of such a parametrized type can be:
_ bit:(## 1) = Bit;
_ 32intwbit:(A Bit) = 32IntWithBit;
In this example, we pass the Bit
type to A
as a parameter.
If you don't want to define a type but still wish to deserialize according to this scheme, you can use the Any
keyword:
_ my_val:(A Any) = Example;
This means that when deserializing the Example
type, we will fetch a 32-bit integer and then parse the remaining bits and references in the cell to assign to my_val
.
You can create complex types with multiple parameters:
_ {X:Type} {Y:Type} my_val:(## 32) next_val:X next_next_val:Y = A X Y;
_ bit:(## 1) = Bit;
_ a_with_two_bits:(A Bit Bit) = AWithTwoBits;
You can also use partial applications with such parametrized types:
_ {X:Type} {Y:Type} v1:X v2:Y = A X Y;
_ bit:(## 1) = Bit;
_ {X:Type} bits:(A Bit X) = BitA X;
You can even apply partial application to parametrized types themselves:
_ {X:Type} v1:X = A X;
_ {X:Type} d1:X = B X;
_ {X:Type} bits:(A (B X)) = AB X;
NAT fields usage for parametrized types
You can use fields defined previously as parameters to types. The serialization will be determined at runtime.
Simple example
_ a:(## 8) b:(## a) = A;
This means that the size of the b
field is stored inside the a
field. When serializing type A
, we first load the 8-bit unsigned integer from the a
field and then use this value to determine the size of the b
field.
This strategy also works for parametrized types:
_ {input:#} c:(## input) = B input;
_ a:(## 8) c_in_b:(B a) = A;
Expression in parametrized types
_ {x:#} value:(## x) = Example (x * 2);
_ _:(Example 4) = 2BitInteger;
In this example, the Example.value
type is determined at runtime.
In the 2BitInteger
definition, we set the value to Example 4
. To determine this type, we use the Example (x * 2)
definition and calculate x
using the formula (y = 2, z = 4)
:
static inline bool mul_r1(int& x, int y, int z) {
return y && !(z % y) && (x = z / y) >= 0;
}
We can also use the addition operator:
_ {x:#} value:(## x) = ExampleSum (x + 3);
_ _:(ExampleSum 4) = 1BitInteger;
In the 1BitInteger
definition, we set the value to ExampleSum 4
. To determine this type, we use the ExampleSum (x + 3)
definition and calculate x
using the formula (y = 3, z = 4)
:
static inline bool add_r1(int& x, int y, int z) {
return z >= y && (x = z - y) >= 0;
}
Negate operator (~
)
Some occurrences of "variables", i.e., already-defined fields, are prefixed by a tilde ~
. This indicates that the variable's occurrence is used oppositely from the default behavior. On the left-hand side of the equation, it means that the variable is deduced (computed) based on this occurrence, rather than substituting its previously calculated value. Conversely, on the right-hand side, the variable is not deduced from the serialized type but will instead be computed during the deserialization process. In other words, a tilde transforms an "input argument" into an "output argument" or vice versa.
A simple example of the negate operator is the definition of a new variable based on another variable:
_ a:(## 32) { b:# } { ~b = a + 100 } = B_Calc_Example;
After defining the new variable, you can use it to pass it to Nat
types:
_ a:(## 8) { b:# } { ~b = a + 10 }
example_dynamic_var:(## b) = B_Calc_Example;
The size of example_dynamic_var
is computed at runtime when we load the a
variable and use its value to determine the size of example_dynamic_var
.
Alternatively, it can be applied to other types:
_ {X:Type} a:^X = PutToRef X;
_ a:(## 32) { b:# } { ~b = a + 100 }
my_ref: (PutToRef b) = B_Calc_Example;
You can also define variables with the negate operator within add or multiply complex expressions:
_ a:(## 32) { b:# } { ~b + 100 = a } = B_Calc_Example;
_ a:(## 32) { b:# } { ~b * 5 = a } = B_Calc_Example;
Negate operator (~
) in type definition
_ {m:#} n:(## m) = Define ~n m;
_ {n_from_define:#} defined_val:(Define ~n_from_define 8) real_value:(## n_from_define) = Example;
Assume we have a class Define ~n m
that takes m
and computes n
by loading it from an m
-bit unsigned integer.
In the Example
type, we store the variable computed by the Define
type into n_from_define
. We also know it's an 8
-bit unsigned integer because we apply the Define
type with Define ~n_from_define 8
. Now, we can use the n_from_define
variable for other kinds to determine the serialization process.
This technique leads to more complex type definitions, such as Unions or Hashmaps.
unary_zero$0 = Unary ~0;
unary_succ$1 {n:#} x:(Unary ~n) = Unary ~(n + 1);
_ u:(Unary Any) = UnaryChain;
This example is explained clearly in the TL-B types article. The main idea here is that UnaryChain
recursively deserializes until it reaches unary_zero$0
because we know the last element of the Unary X
type by the definition unary_zero$0 = Unary ~0
. The value of X
is calculated at runtime due to the definition Unary ~(n + 1)
.
Note: x:(Unary ~n)
means that n
is defined during the serialization process of the Unary
class.
Special types
TVM allows for different types of cells, which include:
- Ordinary
- PrunnedBranch
- Library
- MerkleProof
- MerkleUpdate
By default, all cells are classified as Ordinary
. This applies to all cells described in the TLB as well.
To enable the loading of special types in the constructor, prepend !
before the constructor.
Example
!merkle_update#02 {X:Type} old_hash:bits256 new_hash:bits256
old:^X new:^X = MERKLE_UPDATE X;
!merkle_proof#03 {X:Type} virtual_hash:bits256 depth:uint16 virtual_root:^X = MERKLE_PROOF X;
This technique allows code generation to mark SPECIAL
cells when printing a structure and ensures proper validation of structures with special cells.
Multiple instances of the same type without constructor uniqueness tag check
It is permitted to create multiple instances of the same type, provided only the type parameters differ. In this case, the constructor tag uniqueness check will not be applied.
Example
_ = A 1;
a$01 = A 2;
b$01 = A 3;
_ test:# = A 4;
This means that the A-type parameter determines the actual tag for deserialization:
# class for type `A`
class A(TLBComplex):
class Tag(Enum):
a = 0
b = 1
cons1 = 2
cons4 = 3
cons_len = [2, 2, 0, 0]
cons_tag = [1, 1, 0, 0]
m_: int = None
def __init__(self, m: int):
self.m_ = m
def get_tag(self, cs: CellSlice) -> Optional["A.Tag"]:
tag = self.m_
if tag == 1:
return A.Tag.cons1
if tag == 2:
return A.Tag.a
if tag == 3:
return A.Tag.b
if tag == 4:
return A.Tag.cons4
return None
The same applies when multiple parameters are used:
_ = A 1 1;
a$01 = A 2 1;
b$01 = A 3 3;
_ test:# = A 4 2;
Please note that when adding a parameterized type definition, the tags between the predefined type definition,
e.g.,a
and b
in our example, and the parameterized type definition, e.g., c
in our example, must be unique:
Invalid example
a$01 = A 2 1;
b$11 = A 3 3;
c$11 {X:#} {Y:#} = A X Y;
Valid example
a$01 = A 2 1;
b$01 = A 3 3;
c$11 {X:#} {Y:#} = A X Y;
Comments
The comments follow the same conventions as in C++.
/*
This is
a comment
*/
// This is one line comment
References
- A description of an older version of TL
- block.tlb
- tlbc tool
- CPP Codegen
- tonpy tlb tests
- tonpy py codegen
The documentation is provided by the Disintar team.