Telegram Open Network (TON): документация по FunC

Данная статья является переводом официальной документации Telegram Open Network (TON).

Последнее обновление 02.04.2022г.

Что такое FunC

FunC – это предметно-ориентировочный язык программирования со статической типизацей специально разработанный для написания смарт-контрактов на блокчейне Telegram Open Network (TON).

Смарт-контракт на FunC компилируются в ассемблерный код Fift, который в свою очередь генерирует соответствующий байт-код для виртуальной машины TON. Далее этот байт-код можно использовать для деплоя смарт-контракта в блокчейн или запускать на локальной копии блокчейна TON.

Компиляция смарт-контрактов

Файлы смарт-контрактов, написанные на FunC имеют расширение *.fc.

Чтобы откомпилировать смарт-контракт, прежде нужно установить сам компилятор func. Подробный процесс установки описан в статье “Telegram Open Network (TON): погружение в блокчейн для разработков“.
Когда компилятор установлен, вы можете компилировать смарт-контракты при помощи команды:

func -o output.fif -SPA source0.fc source1.fc ...

Типы данных (Types)

Атомные типы (Atomic types)

  • int — тип 257-битных целых чисел со знаком. По умолчанию в нем уже учтены проверки переполнения, которые приводят к исключениям.
  • cell — соответствует ячейкам памяти в TVM, которые работают по принципу стека. Блокчейн TON хранит постоянные данные в дереве ячеек. Каждая ячейка содержит до 1023 бит произвольных данных и до 4 ссылок на другие ячейки.
  • slice — тип, который является неким представлением среза cell. Причем cell можно преобразовать в slice и уже из slice получить биты данных и ссылки, которые хранились в cell.
  • builder — это тип является сборщиком для значений с типом cell. Биты данных и ссылки могут быть сохранены в builder, а затем сам builder может быть встроен в новую ячейку.
  • tuple — тип для кортежей TVM. Кортеж — это упорядоченная коллекция до 255 компонентов, имеющих значения с произвольными типами.
  • cont — тип продолжения для TVM. cont используются для управления потоком выполнения программы TVM. Это довольно низкоуровневый объект с точки зрения FunC, хотя, как ни парадоксально, довольно общий.

Обратите внимание, что любой из перечисленных выше типов занимает только одну запись в стеке TVM.

Логические типы (Boolean type)

В FunC логические значения представлены целыми числами:

  • false представлено как 0,
  • true представлено как -1 (257 единиц в двоичной записи).

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

Пустые значения (Null values)

Значения Null в FunC это пустые значения для некоторого атомарного типа (см. выше “Atomic types”). В стандартной библиотеке некоторые функции могут возвращать значения как с атомарными типами так и null. Не только атомарные, но и другие типы могут хорошо работать с null, о чем должно быть явно указано в их спецификации. По умолчанию null значения запрещены и приводят к исключению во время выполнения.

Абстрактные типы (Hole type)

FunC поддерживает абстрактный тип _ и var, которые впоследствии могут быть заполнены каким-то фактическим типом самим компилятором во время проверки типов данных у переменных. Например, объявляя “var x = 2“, вы говорите, что “x” равен “2”. Т.к. “2” это число, то компилятор сочтет, что переменная “х” это тоже число.

Составные типы (Composite types).

Типы могут быть включены в состав более сложных комплексных типов данных.

Функциональные типы (Functional type)

Типы вида A -> B представляют собой функции с уточнением типа их входящего аргумента и типа возвращаемого значения. Например, тип int -> cell говорит о том, что на входящим аргументом функии является число, а в ответе TVM возвращает тип cell.

Внутренние значения этого типа представляют из себя тип cont.

Тензорные типы (Tensor types)

Типы значений вида (A, B,…) представляют собой упорядоченные наборы значений типов A, B и т.д., которые все вместе занимают более одной записи стека TVM.

Например, если функция foo имеет тип int -> (int, int), это означает, что функция принимает одно целое число и возвращает два числа. Вызов этой функции может иметь вид (int a, int b) = foo(42);. Внутри функция берет одну запись из стека и устанавливает две записи.

Обратите внимание, что хотя и на низком уровне значение (2, (3, 9)) типа (int, (int, int)) и значение (2, 3, 9) типа (int, int, int) представлены так же, как и три элемента стека 2, 3 и 9, но для проверки типов FunC они являются значениями разных типов: например, code (int a, int b, int c) = (2, (3, 9)); не будет компилироваться.

Частным случаем тензорного типа является единичный тип (). Обычно он используется для представления того факта, что функция не возвращает никакого значения или не имеет аргументов. Например, функция print_int будет иметь тип int -> (), а функция random имеет тип () -> int. Единичный тип () не занимает записей в стеке.

Тип (А) рассматривается средством проверки типов как то же самое, что и тип А.

Кортежные типы (Tuples types)

Типы вида [A, B, …] представляют кортежи в TVM с конкретной длиной, в составе которых находятся известные типы компонентов. Например, [int, cell] — это тип TVM-кортежей, имеющих длину ровно 2, первый компонент которых — целое число, а второй — ячейка cell.

[] — тип пустого кортежа. Обратите внимание, что в отличие от типа unit(), значение [] занимает 1 элемент в стеке.

Полиморфизм с типами переменных

FunC имеет систему типов Миллера-Рабина, поддерживающих полиморфные функции. Например, функция:

forall X -> (X, X) duplicate(X value) {
  return (value, value);
}

является полиморфной, которая на вход принимает одну запись в стеке и возвращает две копии этого значения. Вызов duplicate(6) выдаст две копии значения 6 6. А вызов duplicate([]) выдаст две копии пустого кортежа [] [].

В этом примере X является типом переменной.

Пользовательские типы

В настоящее время FunC не поддерживает другие типы, за исключением тех, которые описаны выше.

Ширина типов

Как вы наверное уже заметили, каждое значение типа занимает определенное количество элементов стека. Если это одно и то же число для всех значений одного типа, то это число называется шириной этого типа. Полиморфные функции в настоящее время могут быть определены только для типов с фиксированной и заранее известной шириной типа.

Комментарии

FunC имеет однострочные комментарии, начинающиеся с ;; (двойной точки с запятой).

int x = 1; ;; assign 1 to x

Есть также многострочные комментарии, который начинается с {- и заканчиваются на -}. Обратите внимание, что, в отличие от многих других языков, многострочные комментарии FunC могут быть вложенными. Например:

{- This is a multi-line comment
    {- this is a comment in the comment -}
-}

Литералы и идентификаторы

Числовые литералы (Number)

FunC допускает десятичные и шестнадцатеричные целые литералы (допускаются ведущие нули). Например, 0, 123, -17, 00987, 0xef, 0xEF, 0x0, -0xfFAb, 0x0001, -0, -0x0 являются допустимыми числовыми литералами.

Строковые литералы (String)

Строки в FunC заключаются в двойные кавычки, например, “это строка”. Специальные символы, такие как \n, и многострочные строки не поддерживаются. Строки используются только в определениях функций asm.

Идентификаторы (Identifiers)

FunC допускает очень широкий класс идентификаторов (имена функций и переменных). А именно, любая (однострочная) строка, которая:

  • не содержит специальных символов ;, ,, (, ),   (пробел или табуляция), ~ и .
  • не начинается как комментарий или как строковый литерал (с )
  • не является числом,
  • не является символом подчеркивания _
  • и не является ключевым словом

является допустимым идентификатором (с тем единственным исключением, что если оно начинается с `, оно должно заканчиваться тем же ` и не может содержать никаких других `, кроме этих двух).

Также имена функций в определениях функций могут начинаться с . или ~.

Для примера, это корректные идентификаторы:

  • query, query’, query”
  • elem0, elem1, elem2
  • CHECK
  • _internal_value
  • message_found?
  • get_pubkeys&signatures
  • dict::udict_set_builder
  • _+_ (стандартный оператор сложения типа (int, int) -> int в префиксной нотации, хотя она уже определена)
  • fatal!

Знак апострафа в конце имени переменной обычно используется, когда вводится некоторая модифицированная версия старого значения. Например, почти все модифицирующие встроенные примитивы для работы с хэш-картой (кроме тех, что имеют префикс ~) берут хэш-карту и при необходимости возвращают новую версию хэш-карты вместе с некоторыми другими данными. Эти значения удобно называть одинаковыми именами с суффиксом .

Что такое суффикс? Он обычно используется для логических переменных (в TVM нет встроенного типа bool; логические значения представлены целыми числами: 0 — ложь, а -1 — истина) или для функций, которые возвращают некоторый флаг, обычно указывающий на успех операции (например, udict_get? из stdlib.fc).

Это недопустимые идентификаторы:

  • take(first)Entry
  • “not_a_string
  • msg.sender
  • send_message,then_terminate
  • _

А вот еще несколько странных примеров допустимых идентификаторов:

  • 123validname
  • 2+2=2*2
  • -alsovalidname
  • 0xefefefhahaha
  • {hehehe}
  • pa{–}in”`aaa`”

А вот еще пример невалидных идентификаторов:

  • pa;;in”`aaa`” (потому что ; запрещено)
  • {-aaa-}
  • aa(bb
  • 123 (это число)

Также FunC имеет специальный тип идентификаторов, которые заключаются в обратные кавычки `. В кавычках допускаются любые символы, кроме \n и самих кавычек.

Например, `я тоже переменная` является допустимым идентификатором, так же, как и `любые символы ; ~ () разрешены здесь…`.

Функции

Программа FunC, по сути, представляет собой список объявлений/определений функций и объявлений глобальных переменных. Рассмотрим первую тему.

Любое объявление или определение функции начинается с общего шаблона, после которого следует одно из трех:

  • точка с запятой ;, что означает, что функция объявлена, но еще не определена. Он может быть определен позже в том же файле или в другом файле, который передается перед текущим компилятору FunC. Это простое объявление функции с именем add и типом (int, int) -> int.
    int add(int x, int y);
  • определение тела функции на ассемблере. Это способ определения функций низкоуровневыми примитивами TVM для последующего использования в программе FunC. Пример ниже является определением на ассемблере той же функции add типа (int, int) -> int, которая преобразуется в TVM в код операции ADD.
    int add(int x, int y) asm "ADD";
  • определение тела функции. Это обычный способ определения функций. Пример ниже является обычным определением функции добавления.
    int add(int x, int y) {
      return x + y;
    }

Объявление функции

Как было сказано ранее, любое объявление или определение функции начинается с общего шаблона. Схема следующая:

[<forall declarator>] <return_type> <function_name>(<comma_separated_function_args>) <specifiers>

где [ … ] соответствуют необязательной записи.

Имя функции

Имя функции может иметь любой идентификатор, а также может начинаться с символов . или ~. Значение этих символов объясняется ниже.

Например, udict_add_builder?, dict_set и ~dict_set допустимы и имеют разные имена функций (они определены в stdlib.fc).

FunC (на самом деле ассемблер Fift) имеет несколько зарезервированных имен функций с предопределенными идентификаторами.

  • main и recv_internal имеет id = 0
  • recv_external имеет id = -1
  • run_ticktock имеет id = -2

В каждой программе должна быть функция с id=0, то есть функция main или recv_internal.

Когда вызываются указанные функции:

  • recv_internal вызывается, когда смарт-контракт получает входящее внутреннее сообщение,
  • recv_external — для входящих внешних сообщений,
  • run_ticktock вызывается в ticktock транзакциях специальных смарт-контрактов.

Тип возвращаемых значений функции

Тип возвращаемого значения может быть любым atomic или composite типом, в соответствии с их описанием в разделе Types выше. Например, допустимыми объявлениями функций, являются следующие:

int foo();
(int, int) foo'();
[int, int] foo''();
(int -> int) foo'''();
() foo''''();

Также разрешена следующая конструкция, где функция pyth принимает 2 аргумента, а возвращает 3 аргумента: (int, int) -> (int, int, int)

_ pyth(int m, int n) {
  return (m * m - n * n, 2 * m * n, m * m + n * n);
}

Аргументы функции

Аргументы функции разделяются запятыми. Допустимы следующие случаи указания аргументов в функция:

  • Каждый аргумент состоит из 2-х сущностей, это тип аргумента и его имя, например int x, пример: () foo(int x);
  • Объявление неиспользуемого аргумента (только тип). Пример корректной функции с типами: (int, int) -> int
    int first(int x, int) {
      return x;
    }
  • Аргумент с объявлением предполагаемого типа. Пример функции int -> int где аргумент изначально неизвестен, но затем компилятор определяет его самостоятельно как тип int.
    int inc(x) {
      return x + 1;
    }

    Обратите внимание: хотя функция может выглядеть как функция c несколькими аргументами, на самом деле это функция может содержать только один аргумент тензорного типа. Тем не менее компоненты тензора аргументов принято называть аргументами функций.

Спецификаторы функций

Существует три типа спецификаторов: impure, inline/inline_ref и method_id. В объявлении функции можно указать один из них, несколько или не указывать их вообще. При указании в одной функции несколькоих спецификаторов, обратите внимание, что они должны быть представлены в правильном порядке: например, нельзя ставить impure после inline.

  • impure спецификатор означает, что функция может иметь некоторые исключения, которые нельзя игнорировать. Например, мы должны указать impure спецификатор, если функция может изменять хранилище контрактов, отправлять сообщения или вызывать исключение, когда некоторые данные могут быть недействительны как и сама функция предназначеная для проверки этих данных.
    Если в функции не указано impure и возвращаемое значение функции не используется, то компилятор FunC может удалить вызов этот функции.
    Например, в функции int random() impure asm "RANDU256"; в stdlib.fc определено, что impure используется потому что RANDU256 изменяет внутреннее состояние генератора случайных чисел.
  • inline спецификатор используется для тех функций, чтобы код этой функции фактически подставляется в каждом месте ее вызова. Таким образом для inline функций рекурсивные вызовы становятся невозможны.
    inline_ref спецификатор указывает, что фунция должна помещается в отдельную ячейку, и каждый раз при вызове функции TVM выполняет команду CALLREF. Это похоже на спецификатор inline, но поскольку ячейку можно повторно использовать в нескольких местах без ее дублирования, то почти всегда использование inline_ref более эффективно с точки зрения размера кода вместо inline, за исключением случая когда функция вызывается только один раз. Рекурсивные вызовы функций  с inline_ref также невозможны, так как в ячейках TVM нет циклических ссылок.
  • method_id(<указанное_значение>)спецификатор позволяет установить идентификатор функции в указанное значение, а method_id без указания значения, позволяет устанавливать индентификатор функции в значение “по умолчанию” (crc16(<function_name>) & 0xffff) | 0x10000. Если у функции есть спецификатор method_id, то она может быть вызвана в lite-client или ton-explorer как get-метод по имени.

    Каждая функция в программе TVM имеет внутренний целочисленный идентификатор, по которому ее можно вызвать. Обычные функции в основном нумеруются последовательными целыми числами, начиная с 1, а get-методы контракта нумеруются хешами crc16 их имени.

    Пример:

    (int, int) get_n_k() method_id {
      (_, int n, int k, _, _, _, _) = unpack_state();
      return (n, k);
    }

Полиморфизм с декларатором типа forall

Перед любым объявлением или определением функции может быть декларатор переменных типа forall. Он имеет следующий синтаксис: forall <comma_separated_type_variables_names> ->, где имя переменной типа может быть любым идентификатором. Хотя обычно они называются заглавными буквами.

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

forall X, Y -> [Y, X] pair_swap([X, Y] pair) {
  [X p1, Y p2] = pair;
  return [p2, p1];
}

pair_swap([2, 3]) будет выводить [3, 2], а pair_swap([1, [2, 3, 4]]) будет выводить [[2, 3, 4], 1].

В этом примере X и Y являются переменными типа. При вызове функции переменные типа заменяются фактическими типами и выполняется код функции. Обратите внимание, что хотя функция является полиморфной, фактический ассемблерный код для нее одинаков для каждой подстановки типа. Это достигается в основном за счет полиморфизма примитивов манипулирования стеком. В настоящее время другие формы полиморфизма (например, специальный полиморфизм с классами типов) не поддерживаются.

Также стоит заметить, что ширина типа X и Y предполагается равной 1, то есть значения X или Y должны занимать одну запись в стеке. Таким образом, вы фактически не можете вызвать функцию pair_swap для кортежа типа [(int, int), int], потому что тип (int, int) имеет ширину 2, т. е. занимает 2 элемента стека.

Функции с реализацией на ассемблере

Функция может быть определена кодом на ассемблере. Синтаксис представляет собой ключевое слово asm, за которым следует одна или несколько команд ассемблера, представленных в виде строк. Например, можно определить функцию, которая увеличивает целое число, а затем инвертирует его. Вызовы этой функции будут преобразованы в 2 ассемблерные команды INC и NEGATE.

int inc_then_negate(int x) asm "INC" "NEGATE";

Данную функцию можно определить альтернативным способом с одной ассемблерной командой INC NEGATE, потому что ассемблер знает, что на самом деле это 2 отдельные команды:

int inc_then_negate'(int x) asm "INC NEGATE";

Изменение порядка записей в стеке

В некоторых случаях мы хотим передать аргументы ассемблерной функции не в том порядке, в котором требует команда ассемблера, и/или получить результат в другом порядке записей стека, чем возвращает команда. Можно было бы вручную изменить порядок стека, добавив соответствующие примитивы стека, но FunC может сделать это автоматически.

Например, предположим, что ассемблерная команда STUXQ принимает integer, builder и integer, а возвращает builder вместе с integer флагагом, указывающий на результат выполнения операции.

(builder, int) store_uint_quite(int x, builder b, int len) asm "STUXQ";

Однако предположим, что мы хотим переставить аргументы.

(builder, int) store_uint_quite(builder b, int x, int len) asm(x b len) "STUXQ";

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

(int, builder) store_uint_quite(int x, builder b, int len) asm( -> 1 0) "STUXQ";

Цифры соответствуют индексам возвращаемых значений (0 — самая глубокая запись в стеке среди возвращаемых значений). Также возможна комбинация этих методов:

(int, builder) store_uint_quite(builder b, int x, int len) asm(x b len -> 1 0) "STUXQ";

(Продолжение статьи будет дополняться и актуализироваться…)

Рейтинг
( 6 оценок, среднее 4.67 из 5 )
Понравилась статья? Поделиться с друзьями:
Добавить комментарий

;-) :| :x :twisted: :smile: :shock: :sad: :roll: :razz: :oops: :o :mrgreen: :lol: :idea: :grin: :evil: :cry: :cool: :arrow: :???: :?: :!: