Перейти к основному содержимому

Язык TL-B

warning

Эта страница переведена сообществом на русский язык, но нуждается в улучшениях. Если вы хотите принять участие в переводе свяжитесь с @alexgton.

TL-B (Type Language - Binary) служит для описания системы типов, конструкторов и существующих функций. Например, мы можем использовать схемы TL-B для построения двоичных структур, связанных с блокчейном TON. Специальные парсеры TL-B могут считывать схемы для десериализации двоичных данных в различные объекты. TL-B описывает схемы данных для объектов Cell. Если вы не знакомы с Cells, пожалуйста, прочтите статью Ячейки пакеты Ячеек (BOC).

Общие сведения

Мы называем любой набор конструкций TL-B документами TL-B. Документ TL-B обычно состоит из объявлений типов ( т. е. их конструкторов) и функциональных комбинаторов. Объявление каждого комбинатора заканчивается точкой с запятой (;).

Вот пример возможного объявления комбинатора:



Конструкторы

Левая часть каждого уравнения описывает способ определения или сериализации значения типа, указанного в правой части. Такое описание начинается с имени конструктора.



Конструкторы используются для указания типа комбинатора, включая состояние при сериализации. Например, конструкторы также могут использоваться, когда вы хотите указать op(код операции) в запросе к смарт-контракту в TON.

// ....
transfer#5fcc3d14 <...> = InternalMsgBody;
// ....
  • имя конструктора: transfer
  • префиксный код конструктора: #5fcc3d14

Обратите внимание, что за каждым именем конструктора сразу следует необязательный тег конструктора, такой как #_ или $10, который описывает битовую строку, используемую для кодирования (сериализации) рассматриваемого конструктора.

message#3f5476ca value:# = CoolMessage;
bool_true$0 = Bool;
bool_false$1 = Bool;

Левая часть каждого уравнения описывает способ определения или сериализации значения типа, указанного в правой части. Такое описание начинается с имени конструктора, например message или bool_true, за которым сразу следует необязательный тег конструктора, например #3f5476ca или $0, который описывает биты, используемые для кодирования ( сериализации) рассматриваемого конструктора.

конструкторсериализация
some#3f5476ca32-битный uint сериализуется из шестнадцатеричного значения
some#5fe12-битный uint сериализуется из шестнадцатеричного значения
some$0101сериализует 0101 необработанные биты
some или some#сериализует crc32(уравнение) | 0x80000000
some#_ или some$_ или _не сериализуется

Имена конструкторов (some в этом примере) используются как переменные в codegen. Например:

bool_true$1 = Bool;
bool_false$0 = Bool;

Тип Bool имеет два тега 0 и 1. Псевдокод Codegen может выглядеть так:


class Bool:
tags = [1, 0]
tags_names = ['bool_true', 'bool_false']

Если вы не хотите определять имя для текущего конструктора, просто передайте _, например, _ a:(## 32) = 32Int;

Теги конструктора могут быть заданы либо в двоичной (после знака доллара), либо в шестнадцатеричной нотации (после знака решетки). Если тег не указан явно, анализатор TL-B должен вычислить 32-битный тег конструктора по умолчанию, хешируя с помощью алгоритма CRC32 текст "уравнения" с | 0x80000000, определяя этот конструктор определенным образом. Поэтому пустые теги должны быть явно указаны #_ или $_.

Этот тег будет использоваться для определения текущего типа битовой строки в процессе десериализации. Например, у нас есть 1 битовая строка 0, если мы скажем TLB проанализировать эту битовую строку в типе Bool, он проанализирует ее как Bool.bool_false.

Допустим, у нас есть более сложные примеры:

tag_a$10 val:(## 32) = A;
tag_b$00 val(## 64) = A;

Если мы проанализируем 1000000000000000000000000000000000000001 (1 и 32 нуля и 1) в типе TLB A - сначала нам нужно получить первые два бита для определения тега. В этом примере 10 - это два первых бита, и они представляют tag_a. Теперь мы знаем, что следующие 32 бита - это переменная val, 1 в нашем примере. Некоторые "проанализированные" переменные псевдокода могут выглядеть так:

A.tag = 'tag_a'
A.tag_bits = '10'
A.val = 1

Все имена конструкторов должны быть разными, а теги конструкторов для одного типа должны составлять префиксный код (иначе десериализация не будет уникальной); т. е. ни один тег не может быть префиксом любого другого в том же типе.

Максимальное количество конструкторов для одного типа: 64 Максимальное количество бит для тега: 63

Двоичный пример:
example_a$10 = A;
example_b$01 = A;
example_c$11 = A;
example_d$00 = A;

Псевдокод Codegen может выглядеть так:


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;

Псевдокод Codegen может выглядеть так:


class A:
tags = [0, 1, 15]
tags_names = ['example_a', 'example_b', 'example_c']

Если вы используете тег hex, имейте в виду, что он будет сериализован как 4 бита для каждого шестнадцатеричного символа. Максимальное значение — 63-битное целое число без знака. Это означает:

a#32 a:(## 32) = AMultiTagInt;
b#1111 a:(## 32) = AMultiTagInt;
c#5FE a:(## 32) = AMultiTagInt;
d#3F5476CA a:(## 32) = AMultiTagInt;
конструкторсериализация
a#328-битный uint сериализуется из шестнадцатеричного значения
b#111116-битный uint сериализуется из шестнадцатеричного значения
c#5FE12-битный uint сериализуется из шестнадцатеричного значения
d#3F5476CA32-битный uint сериализуется из шестнадцатеричного значения

Также шестнадцатеричные значения разрешены как в верхнем, так и в нижнем регистре.

Подробнее о шестнадцатеричных тегах

В дополнение к классическому определению шестнадцатеричного тега, за шестнадцатеричным числом может следовать символ подчеркивания. Это означает, что тег равен указанному шестнадцатеричному числу без младшего бита. Например, есть такая схема:

vm_stk_int#0201_ value:int257 = VmStackValue;

И тег на самом деле не равен 0x0201. Чтобы вычислить его, нам нужно удалить LSb из двоичного представления 0x0201:

0000001000000001 -> 000000100000000

Таким образом, тег равен 15-битному двоичному числу 0b000000100000000.

Определения полей

За конструктором и его необязательным тегом следуют определения полей. Каждое определение поля имеет форму ident:type-expr, где ident — это идентификатор с именем поля (заменяется на подчеркивание для анонимных полей), а type-expr — это тип поля. Тип, представленный здесь, является выражением типа, которое может включать простые типы, параметризованные типы с подходящими параметрами или сложные выражения.

В итоге все поля, определенные в типе, не должны быть больше, чем Cell (1023 бит и 4 ссылки)

Простые типы

  • _ a:# = Type; - Type.a здесь 32-битное целое число
  • _ a:(## 64) = Type; - Type.a здесь 64-битное целое число
  • _ a:Owner = NFT; - NFT.a здесь тип Owner
  • _ a:^Owner = NFT; - NFT.a здесь ссылка на ячейку типа Owner означает, что Owner хранится в следующей ссылке на ячейку.

Анонимные поля

  • _ _:# = A; - первое поле - анонимное 32-битное целое число

Расширяем ячейку ссылками

_ a:(##32) ^[ b:(## 32) c:(## 32) d:(## 32)] = A;
  • Если по какой-то причине мы хотим отделить некоторые поля в другую ячейку, мы можем использовать синтаксис ^[ ... ]. В этом примере A.a / A.b / A.c / A.d - это 32-битные целые числа без знака, но A.a хранится в первой ячейке, а A.b / A.c / A.d хранятся в следующей ячейке (1 ссылка)
_ ^[ a:(## 32) ^[ b:(## 32) ^[ c:(## 32) ] ] ] = A;
  • Также допускаются цепочки ссылок. В этом примере каждая из переменные (a, b, c) хранятся в отдельных ячейках

Параметризованные типы

Предположим, у нас есть тип IntWithObj:

_ {X:Type} a:# b:X = IntWithObj X;

Теперь мы можем использовать его в других типах:

_ a:(IntWithObj uint32) = IntWithUint32;

Сложные выражения

  • Условные поля (только для Nat) (E?T означает, что если выражение E истинно, то поле имеет тип T)

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

    В типе Example переменная b сериализуется, только если a равно 1

  • Выражение умножения для создания кортежей (x * T означает создание кортежа длины x типа T):

    a$_ a:(## 32) = A;
    b$_ b:(2 * A) = B;
    _ (## 1) = Bit;
    _ 2bits:(2 * Bit) = 2Bits;
  • Выборка бита (только для Nat) (E . B означает взятие бита B из Nat E)

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

    В типе Example переменная b сериализуется, только если второй бит a равен 1

  • Также разрешены другие операторы Nat (см. Разрешенные ограничения)

Примечание: можно объединить несколько сложных выражений:

_ a:(## 1) b:(## 1) c:(## 2) d:(a?(b?((c . 1)?(## 64)))) = A;

Встроенные типы

  • # - Nat 32-битное целое число без знака
  • ## x - Nat с x битами
  • #< x - Nat меньше x бит целое число без знака, сохраненное как lenBits(x - 1) бит, до 31 бита
  • #<= x - Nat меньше или равно x бит целое число без знака, сохраненное как lenBits(x) бит, до 32 бит
  • Any / Cell - остальные биты и ссылки ячейки
  • Int - 257 бит
  • UInt - 256 бит
  • Bits - 1023 бита
  • uint1 - uint256 - 1 - 256 бит
  • int1 - int257 - 1 - 257 бит
  • bits1 - bits1023 - 1 - 1023 бита
  • uint X / int X / bits X - то же, что и uintX, но в этих типах можно использовать параметризованный X

Ограничения

_ flags:(## 10) { flags <= 100 } = Flag;

Поля Nat разрешены в ограничениях. В этом примере ограничение { flags <= 100 } означает, что переменная flags меньше или равна 100.

Допустимые ограничения: E | E = E | E <= E | E < E | E >= E | E > E | E + E | E * E | E ? E

Неявные поля

Некоторые поля могут быть неявными. Их определения заключены в фигурные скобки ({, }), которые указывают, что поле фактически не присутствует в сериализации, но его значение должно быть выведено из других данных (обычно параметров сериализуемого типа). Пример:

nothing$0 {X:Type} = Maybe X;
just$1 {X:Type} value:X = Maybe X;
_ {x:#} a:(## 32) { ~x = a + 1 } = Example;

Параметризованные типы

Переменные — т. е. (идентификаторы) ранее определенных полей типов # (натуральные числа) или Type (тип типов) — могут использоваться в качестве параметров для параметризованных типов. Процесс сериализации рекурсивно сериализует каждое поле в соответствии с его типом, а сериализация значения в конечном итоге состоит из объединения битов, представляющих конструктор (т. е. тег конструктора), и значений полей.

Натуральные числа (Nat)

_ {x:#} my_val:(## x) = A x;

Означает, что A параметризуется с помощью x Nat. В процессе десериализации мы получим x-битное целое число без знака, например.:

_ value:(A 32) = My32UintValue;

Это означает, что в процессе десериализации типа My32UintValue мы получим 32-битное целое число без знака (из-за 32 параметр для типа A)

Типы

_ {X:Type} my_val:(## 32) next_val:X = A X;

Означает, что A параметризуется типом X. В процессе десериализации мы выберем 32-битное целое число без знака, а затем проанализируем биты и ссылки типа X.

Пример использования такого параметризованного типа может быть следующим:

_ bit:(## 1) = Bit;
_ 32intwbit:(A Bit) = 32IntWithBit;

В этом примере мы передаем тип Bit в A в качестве параметра.

Если вы не хотите определять тип, но хотите десериализовать по этой схеме, вы можете использовать слово Any:

_ my_val:(A Any) = Example;

Означает, что если мы десериализуем тип Example, мы извлечем 32-битное целое число, а затем остаток ячейки (bits&refs) в my_val.

Вы можете создавать сложные типы с несколькими параметрами:

_ {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;

Также вы можете использовать частичное применение к таким параметризованным типам:

_ {X:Type} {Y:Type} v1:X v2:Y = A X Y;
_ bit:(## 1) = Bit;
_ {X:Type} bits:(A Bit X) = BitA X;

Или даже к самим параметризованным типам:

_ {X:Type} v1:X = A X;
_ {X:Type} d1:X = B X;
_ {X:Type} bits:(A (B X)) = AB X;

Использование полей NAT для параметризованных типов

Вы можете использовать поля, определенные ранее, как параметры для типов. Сериализация будет определена во время выполнения.

Простой пример:

_ a:(## 8) b:(## a) = A;

Это означает, что мы сохраняем размер поля b внутри поля a. Поэтому, когда мы хотим сериализовать тип A, нам нужно загрузить 8 битное беззнаковое целое число поля a, а затем использовать это число для определения размера поля b.

Эта стратегия также работает для параметризованных типов:

_ {input:#} c:(## input) = B input;
_ a:(## 8) c_in_b:(B a) = A;

Выражение в параметризованных типах

_ {x:#} value:(## x) = Example (x * 2);
_ _:(Example 4) = 2BitInteger;

В этом примере тип "Example.value" определяется во время выполнения.

В определении 2BitInteger мы устанавливаем значение типа Example 4. Для определения этого типа мы используем определение Example (x * 2) и вычисляем x по формуле (y = 2, z = 4):

static inline bool mul_r1(int& x, int y, int z) {
return y && !(z % y) && (x = z / y) >= 0;
}

Мы также можем использовать оператор сложения:

_ {x:#} value:(## x) = ExampleSum (x + 3);
_ _:(ExampleSum 4) = 1BitInteger;

В определении 1BitInteger мы устанавливаем значение типа ExampleSum 4. Для определения этого типа мы используем определение ExampleSum (x + 3) и вычисляем x по формуле (y = 3, z = 4):

static inline bool add_r1(int& x, int y, int z) {
return z >= y && (x = z - y) >= 0;
}

Оператор отрицания (~)

Некоторые вхождения "переменных" (т. е. уже определенных полей) имеют префикс тильды (~). Это указывает на то, что использование переменной происходит в противоположном направлении по сравнению с обычным поведением: слева от уравнения означает, что переменная будет выведена (вычислена) на основе этого вхождения, вместо подстановки её ранее вычисленного значения; справа же это означает, что переменная не будет выводиться из типа, который мы сериализуем, а будет вычисляться во время процесса десериализации. В других словах, тильда преобразует "входной аргумент" в "выходной аргумент" или наоборот.

Простой пример оператора отрицания — определение новой переменной на основе другой переменной:

_ a:(## 32) { b:# } { ~b = a + 100 } = B_Calc_Example;

После определения вы можете использовать новую переменную для передачи ее типам Nat:

_ a:(## 8) { b:# } { ~b = a + 10 }
example_dynamic_var:(## b) = B_Calc_Example;

Размер example_dynamic_var будет вычислен во время выполнения, когда мы загружаем переменную a и используем ее значение для определения размера example_dynamic_var.

Или для других типов:

_ {X:Type} a:^X = PutToRef X;
_ a:(## 32) { b:# } { ~b = a + 100 }
my_ref: (PutToRef b) = B_Calc_Example;

Также вы можете определять переменные с оператором отрицания в сложных выражениях сложения или умножения:

_ a:(## 32) { b:# } { ~b + 100 = a }  = B_Calc_Example;
_ a:(## 32) { b:# } { ~b * 5 = a }  = B_Calc_Example;

Оператор отрицания (~) в определении типа

_ {m:#} n:(## m) = Define ~n m;
_ {n_from_define:#} defined_val:(Define ~n_from_define 8) real_value:(## n_from_define) = Example;

Предположим, у нас есть класс Define ~n m, который принимает m и вычисляет n, загружая его из m битового целого числа без знака.

В типе Example мы сохраняем переменную, вычисленную типом Define, в n_from_define, также мы знаем, что это 8 битовое целое число без знака, потому что мы применяем тип Define с Define ~n_from_define 8. Теперь мы можем использовать переменную n_from_define в других типах для определения процесса сериализации.

Эта техника приводит к более сложным определениям типов (таким как Unions, Hashmaps).

unary_zero$0 = Unary ~0;
unary_succ$1 {n:#} x:(Unary ~n) = Unary ~(n + 1);
_ u:(Unary Any) = UnaryChain;

Этот пример хорошо объяснен в статье Типы TL-B . Основная идея здесь заключается в том, что UnaryChain будет рекурсивно десериализоваться до достижения unary_zero$0 (потому что мы знаем последний элемент типа Unary X по определению unary_zero$0 = Unary ~0;, а X вычисляется во время выполнения из-за определения Unary ~(n + 1)).

Примечание: x:(Unary ~n) означает, что n определяется в процессе сериализации класса Unary.

Специальные типы

В настоящее время TVM допускает типы ячеек:

  • Ordinary
  • PrunnedBranch
  • Library
  • MerkleProof
  • MerkleUpdate

По умолчанию все ячейки являются Ordinary. И все ячейки, описанные в tlb, являются Ordinary.

Чтобы разрешить загрузку специальных типов в конструкторе, вам нужно добавить ! перед конструктором.

Пример:

!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;

Эта техника позволяет коду codegen отмечать ячейки SPECIAL, когда вы хотите распечатать структуру, а также позволяет правильно проверять структуры со специальными ячейками.

Несколько экземпляров одного типа без проверки уникальности тега конструктора

Разрешается создавать несколько экземпляров одного типа в зависимости только от параметров типа. При таком способе определения, проверка тега уникальности конструктора применяться не будет.

Пример:

_ = A 1;
a$01 = A 2;
b$01 = A 3;
_ test:# = A 4;

Означает, что фактический тег для десериализации будет определяться параметром типа A:

# 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

То же самое работает с несколькими параметрами:

_ = A 1 1;
a$01 = A 2 1;
b$01 = A 3 3;
_ test:# = A 4 2;

Обратите внимание, что при добавлении определения параметризованного типа теги между предопределенным определением типа (a и b в нашем примере) и параметризованным определением типа (c в нашем примере) должны быть уникальными:

Недопустимый пример:

a$01 = A 2 1;
b$11 = A 3 3;
c$11 {X:#} {Y:#} = A X Y;

Допустимый пример:

a$01 = A 2 1;
b$01 = A 3 3;
c$11 {X:#} {Y:#} = A X Y;

Комментарии

Комментарии такие же, как в C++

/*
This is
a comment
*/

// This is one line comment

Полезные источники


Документация предоставлена командой Disintar.