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

Функции

warning

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

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

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

  • одиночный ;, что означает, что функция объявлена, но еще не определена. Она может быть определена позже в том же файле или в каком-либо другом файле, который передается компилятору FunC перед текущим. Например,

    int add(int x, int y);

    это простое объявление функции с именем add типа (int, int) -> int.

  • определение тела функции ассемблера. Это способ определения функций с помощью примитивов TVM низкого уровня для последующего использования в программе FunC. Например,

    int add(int x, int y) asm "ADD";

    это ассемблерное определение той же функции add типа (int, int) -> int, которое будет транслироваться в код операции TVM ADD.

  • обычное определение тела функции блочного оператора. Это обычный способ определения функций. Например,

    int add(int x, int y) {
    return x + y;
    }

    это обычное определение функции add.

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

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

[<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. run_ticktock вызывается в транзакциях ticktock специальных смарт-контрактов.

Внутренние получения

recv_internal вызывается, когда смарт-контракт получает входящее внутреннее сообщение. При запуске TVM в стеке есть несколько переменных, задавая аргументы в recv_internal, мы даем коду смарт-контракта информацию о некоторых из них. Те аргументы, о которых код не будет знать, просто будут лежать внизу стека и никогда не будут затронуты.

Итак, каждое из следующих объявлений recv_internal является правильным, но те, у которых меньше переменных, будут тратить немного меньше газа (каждый неиспользованный аргумент добавляет дополнительные инструкции DROP)


() recv_internal(int balance, int msg_value, cell in_msg_cell, slice in_msg) {}
() recv_internal(int msg_value, cell in_msg_cell, slice in_msg) {}
() recv_internal(cell in_msg_cell, slice in_msg) {}
() recv_internal(slice in_msg) {}

Внешние получения

recv_external предназначено для входящих внешних сообщений.

Тип возврата

Тип возврата может быть любым атомарным или составным типом, как описано в разделе типы. Например,

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

являются допустимыми объявлениями функций.

Вывод типа также допускается. Например,

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

является допустимым определением функции pyth типа (int, int) -> (int, int, int), которая вычисляет пифагоровы тройки.

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

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

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

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

Вызовы функций

Немодифицирующие методы

к сведению

Немодифицирующая функция поддерживает короткую форму вызова функции с .

example(a);
a.example();

Если у функции есть хотя бы один аргумент, ее можно вызвать как немодифицирующий метод. Например, store_uint имеет тип (builder, int, int) -> builder (второй аргумент — это значение для сохранения, а третий — длина в битах). begin_cell — это функция, которая создает новый builder. Следующие коды эквивалентны:

builder b = begin_cell();
b = store_uint(b, 239, 8);
builder b = begin_cell();
b = b.store_uint(239, 8);

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

builder b = begin_cell().store_uint(239, 8);

Также возможны множественные вызовы методов:

builder b = begin_cell().store_uint(239, 8)
.store_int(-1, 16)
.store_uint(0xff, 10);

Модифицирующие функции

к сведению

Модифицирующая функция поддерживает краткую форму с операторами ~ и ..

Если первый аргумент функции имеет тип A, а возвращаемое значение функции имеет вид (A, B), где B — некоторый произвольный тип, то функцию можно вызвать как модифицирующий метод.

Модифицирующие вызовы функций могут принимать некоторые аргументы и возвращать некоторые значения, но они изменяют свой первый аргумент, то есть присваивают первый компонент возвращаемого значения переменной из первого аргумента.

a~example();
a = example(a);

Например, предположим, что cs — это срез ячейки, а load_uint имеет тип (slice, int) -> (slice, int): он принимает срез ячейки и количество бит для загрузки и возвращает остаток среза и загруженное значение. Следующие коды эквивалентны:

(cs, int x) = load_uint(cs, 8);
(cs, int x) = cs.load_uint(8);
int x = cs~load_uint(8);

В некоторых случаях мы хотим использовать функцию в качестве модифицирующего метода, который не возвращает никакого значения и изменяет только первый аргумент. Это можно сделать, используя типы единиц измерения, следующим образом: предположим, мы хотим определить функцию inc типа int -> int, которая увеличивает целое число, и использовать ее в качестве метода модификации. Затем мы должны определить inc как функцию типа int -> (int, ()):

(int, ()) inc(int x) {
return (x + 1, ());
}

При таком определении его можно использовать как модифицирующий метод. Следующий пример увеличит x.

x~inc();

. и ~ в именах функций

Предположим, мы хотим использовать inc как немодифицирующий метод. Мы можем написать что-то вроде этого:

(int y, _) = inc(x);

Однако можно переопределить определение inc, используя модифицирующий метод.

int inc(int x) {
return x + 1;
}
(int, ()) ~inc(int x) {
return (x + 1, ());
}

А затем назовем это так:

x~inc();
int y = inc(x);
int z = x.inc();

Первый вызов изменит x; второй и третий — нет.

Подводя итог, когда функция с именем foo вызывается как немодифицирующий или модифицирующий метод (т. е. с синтаксисом .foo или ~foo), компилятор FunC использует определение .foo или ~foo соответственно, если такое определение представлено, а если нет, то использует определение foo.

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

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

Спецификатор с побочными эффектами

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

Если impure не указан и результат вызова функции не используется, то компилятор FunC может и удалить этот вызов функции.

Например, в функции stdlib.fc

int random() impure asm "RANDU256";

определено. impure используется, потому что RANDU256 изменяет внутреннее состояние генератора случайных чисел.

Встроенный спецификатор

Если функция имеет спецификатор inline, ее код фактически подставляется в каждом месте, где вызывается функция. Само собой разумеется, что рекурсивные вызовы встроенных функций невозможны.

Например,

(int) add(int a, int b) inline {
return a + b;
}

поскольку функция add отмечена спецификатором inline. Компилятор попытается заменить вызовы add фактическим кодом a + b, избегая накладных расходов на вызов функции.

Вот еще один пример того, как можно использовать встроенный спецификатор, взятый из ICO-Minter.fc:

() save_data(int total_supply, slice admin_address, cell content, cell jetton_wallet_code) impure inline {
set_data(begin_cell()
.store_coins(total_supply)
.store_slice(admin_address)
.store_ref(content)
.store_ref(jetton_wallet_code)
.end_cell()
);
}

Спецификатор Inline_ref

Код функции со спецификатором inline_ref помещается в отдельную ячейку, и каждый раз, когда вызывается функция, TVM выполняет команду CALLREF. Таким образом, это похоже на inline, но поскольку ячейку можно повторно использовать в нескольких местах, не дублируя ее, почти всегда более эффективно с точки зрения размера кода использовать спецификатор inline_ref вместо inline, если только функция не вызывается ровно один раз. Рекурсивные вызовы встроенных функций по-прежнему невозможны, поскольку в ячейках TVM нет циклических ссылок.

method_id

Каждая функция в программе TVM имеет внутренний целочисленный идентификатор, по которому она может быть вызвана. Обычные функции нумеруются последующими целыми числами, начиная с 1, но get методы контракта нумеруются хэшами crc16 их имен. спецификатор method_id(<some_number>) позволяет присвоить идентификатору функции указанное значение, а method_id использует значение по умолчанию (crc16(<function_name>) & 0xffff) | 0x10000. Если функция имеет спецификатор method_id, то она может быть вызвана в lite-client или ton-explorer как get-метод по своему имени.

Например:

int get_counter() method_id {
load_data();
return ctx_counter;
}

Полиморфизм с forall

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

forall <comma_separated_type_variables_names> ->

где имя переменной типа может быть любым идентификатором. Обычно они именуются заглавными.

Например,

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

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

pair_swap([2, 3]) вернет [3, 2], а pair_swap([1, [2, 3, 4]]) вернет [[2, 3, 4], 1].

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

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

Определение тела функции ассемблера

Как упоминалось выше, функция может быть определена кодом ассемблера. Синтаксис представляет собой ключевое слово asm, за которым следует одна или несколько команд ассемблера, представленных в виде строк. Например, можно определить:

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

– функция, которая увеличивает целое число, а затем инвертирует его. Вызовы этой функции будут транслироваться в 2 команды ассемблера INC и NEGATE. Альтернативный способ определения функции:

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

INC NEGATE будет рассматриваться FunC как одна команда ассемблера, но это нормально, поскольку ассемблер Fift знает, что это 2 отдельные команды.

к сведению

Список команд ассемблера можно найти здесь: инструкции TVM.

Перестановка записей стека

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

к сведению

Обратите внимание, что в случае ручной перестановки аргументы будут вычисляться в переставленном порядке. Чтобы переопределить это поведение, используйте #pragma compute-asm-ltr: compute-asm-ltr

Например, предположим, что команда на ассемблере STUXQ принимает целое число, 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";

Многострочные asm

Многострочные команды ассемблера или даже фрагменты Fift-кода можно определить с помощью многострочных строк, которые начинаются и заканчиваются """.

slice hello_world() asm """
"Hello"
" "
"World"
$+ $+ $>s
PUSHSLICE
""";