Skip to main content

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.

ConstructorSerialization
some#3f5476caA 32-bit uint is serialized from a hex value.
some#5feA 12-bit uint is serialized from a hex value.
some$0101Serialize 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.
Binary example:
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']
Hex tag example:
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;
ConstructorSerialization
a#32A 8-bit uint is serialized from a hex value.
b#1111A 16-bit uint is serialized from a hex value.
c#5FEA 12-bit uint is serialized from a hex value.
d#3F5476CAA 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 type Owner.
  • _ a:^Owner = NFT; - NFT.a is a cell ref to Owner type - i.e., the Owner 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, while A.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 condition E is true, then the field has the type T.

    Example

    _ a:(## 1) b:a?(## 32) = Example;

    In Example type, the field b is serialized only if a is equal to 1.

  • Multiplicative expression for tuple creation

    The expression x * T creates a tuple of length x, where each element is of type T.

    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 bit B from the Nat value E.

    Example

    _ a:(## 2) b:(a . 1)?(## 32) = Example;

    In Example type, the variable b is serialized only if the second bit of a is 1.

  • Other Nat operators

    Other operations on Nat types are also supported (refer to Allowed 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 with x bits.
  • #< x - Nat: unsigned integer less than x bits, stored as lenBits(x - 1) bits up to 31 bits.
  • #<= x - Nat: unsigned integer less than or equal to x bits, stored as lenBits(x) bits up to 32 bits.
  • Any / Cell - remaining bits and references.
  • Int - 257 bits
  • UInt - 256 bits
  • Bits - 1023 bits
  • uint1 - uint256 - 1 - 256 bits
  • int1 - int257 - 1 - 257 bits
  • bits1 - bits1023 - 1 - 1023 bits
  • uint X / int X / bits X - same as uintX but can use a parametrized X 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


The documentation is provided by the Disintar team.

Was this article useful?