Сравнение Tolk и FunC: подробно
Эта страница переведена сообществом на русский язык, но нужд ается в улучшениях. Если вы хотите принять участие в переводе свяжитесь с @alexgton.
Ниже очень большой список. У кого-нибудь хватит терпения дочитать его до конца?..
✅ Классические комментарии :)
FunC | Tolk |
---|---|
;; комментарий | // комментарий |
{- многострочный комментарий -} | /* многострочный комментарий */ |
✅ 2+2
— это 4, а не идентификатор. Идентификаторы могут быть только буквенно-цифровыми
В FunC почти любой символ может быть частью идентификатора.
Например, 2+2
(без пробела) — это идентификатор.
Вы даже можете объявить переменную с таким именем.
В Tolk пробелы не обязательны. 2+2
— это 4, как и ожидалось. 3+~x
это 3 + (~ x)
и так далее.
FunC | Tolk |
---|---|
return 2+2; ;; неопределенная функция `2+2` | return 2+2; // 4 |
Точнее, идентификатор может начинаться с [a-zA-Z$_]
и продолжиться с [a-zA-Z0-9$_]
. Обратите внимание, что ?
, :
и другие не являются допустимыми символами, found?
и op::increase
не являются допустимыми идентификаторами.
Вы можете использовать обратные кавычки, чтобы окружить идентификатор, и тогда он может содержать любые символы (аналогично Kotlin и некоторым другим языкам). Его потенциальное применение — разрешить использовать ключевые слова в качестве идентификаторов, например, в случае генерации кода по схеме.
FunC | Tolk |
---|---|
const op::increase = 0x1234; | const OP_INCREASE = 0x1234; |
;; even 2%&!2 is valid | // don't do like this :) |
✅ По умолчанию нечеткий, компилятор не будет отбрасывать вызовы пользовательских функций
FunC имеет спецификатор функции impure
. При его отсутствии функция рассматривается как чистая. Если ее результат не используется, ее вызов был удален компилятором.
Хотя такое поведение документировано, оно очень неожиданно для новичков. Например, различные функции, которые ничего не возвращают (например, выдают исключение при несовпадении), молча удаляются. Эту ситуацию портит тот факт, что FunC не проверяет и не валидирует тело функции, разрешая нечеткие операции внутри чистых функций.
В Tolk все функции по умолчанию являются нечистыми. Вы можете пометить функцию как чистую с помощью аннотации, и тогда в ее теле будут запрещены нечистые операции (исключения, изменение глобальных параметров, вызов нечистых функций и т. д.).
✅ Новый синтаксис функций: ключевое слово fun
, атрибуты @
, типы справа (как в TypeScript, Kotlin, Python и т. д.)
FunC | Tolk |
---|---|
cell parse_data(slice cs) { } | fun parse_data(cs: slice): cell { } |
(cell, int) load_storage() { } | fun load_storage(): (cell, int) { } |
() main() { ... } | fun main() { ... } |
Типы переменных — также справа:
FunC | Tolk |
---|---|
slice cs = ...; | var cs: slice = ...; |
(cell c, int n) = parse_data(cs); | var (c: cell, n: int) = parse_data(cs); |
global int stake_at; | global stake_at: int; |
Модификаторы inline
и другие — с аннотациями:
FunC | Tolk |
---|---|
| @inline |
| @inline_ref |
global int stake_at; | global stake_at: int; |
forall
— таким образом:
FunC | Tolk |
---|---|
forall X -> tuple cons(X head, tuple tail) | fun cons<X>(head: X, tail: tuple): tuple |
реализация asm
— как в FunC, но при правильном выравнивании выглядит красивее:
@pure
fun third<X>(t: tuple): X
asm "THIRD";
@pure
fun iDictDeleteGet(dict: cell, keyLen: int, index: int): (cell, slice, int)
asm(index dict keyLen) "DICTIDELGET NULLSWAPIFNOT";
@pure
fun mulDivFloor(x: int, y: int, z: int): int
builtin;
Также есть атрибут @deprecated
, не влияющий на компиляцию, но для человека и IDE.
✅ get
вместо method_id
В FunC method_id
(без аргументов) фактически объявляет get-метод. В Tolk используется простой синтаксис:
FunC | Tolk |
---|---|
int seqno() method_id { ... } | get seqno(): int { ... } |
Допустимы как get methodName()
, так и get fun methodName()
.
Для method_id(xxx)
(на практике встречается редко, но допустимо) есть атрибут:
FunC | Tolk |
---|---|
| @method_id(1666) |
✅ Важно объявить типы параметров (хотя необязательно для локальных переменных)
// not allowed
fun do_smth(c, n)
// types are mandatory
fun do_smth(c: cell, n: int)
Если типы параметров обязательны, возвращаемый тип не является обязательным (это часто очевидно из-за многословности). Если он опущен, он выводится автоматически:
fun x() { ... } // auto infer from return statements
Для локальных переменных типы также необязательны:
var i = 10; // ok, int
var b = beginCell(); // ok, builder
var (i, b) = (10, beginCell()); // ok, two variables, int and builder
// types can be specified manually, of course:
var b: builder = beginCell();
var (i: int, b: builder) = (10, beginCell());
✅ Переменные не могут быть повторно объявлены в той же области
var a = 10;
...
var a = 20; // error, correct is just `a = 20`
if (1) {
var a = 30; // it's okay, it's another scope
}
Как следствие, частичное переназначение не допускается:
var a = 10;
...
var (a, b) = (20, 30); // error, releclaration of a
Обратите внимание, что это не проблема для loadUint()
и других методов. В FunC они возвращали измененный объект, поэтому шаблон var (cs, int value) = cs.load_int(32)
был довольно распространен. В Tolk такие методы изменяют объект: var value = cs.loadInt(32)
, поэтому повторное объявление вряд ли понадобится.
fun send(msg: cell) {
var msg = ...; // error, redeclaration of msg
// solution 1: intruduce a new variable
var msgWrapped = ...;
// solution 2: use `redef`, though not recommended
var msg redef = ...;
✅ Изменения в системе типов
Система типов FunC основана на Хиндли-Милнере. Это распространенный подход для функциональных языков, где типы выводятся из использования посредством унификации.
В Tolk v0.7 система типов переписана с нуля. Чтобы добавить логические значения, целые числа фиксированной ширины, допустимость значений NULL, структуры и обобщения, нам нужна статическая система типов (например, TypeScript или Rust). Поскольку Хиндли-Милнер будет конфликтовать с методами структур, бороться с надлежащими обобщениями и станет совершенно непрактичным для типов объединений (несмотря на заявления, что он был "разработан для типов объединений").
У нас есть следующие типы:
int
,bool
,cell
,slice
,builder
, нетипизированныйtuple
- типизированный кортеж
[T1, T2, ...]
- тензор
(T1, T2, ...)
- вызываемые
fun(TArgs) -> TResult
void
(каноничнее называтьunit
, ноvoid
надежнее)self
, для создания цепочечных методов, описанных ниже; на самом деле это не тип, он может встречаться только вместо возвращаемого типа функции
Система типов подчиняется следующим правилам:
- типы переменных могут быть указ аны вручную или выведены из объявлений и никогда не изменяются после объявления
- параметры функции должны быть строго типизированы
- возвращаемые типы функции, если не указаны, выводятся из операторов return, аналогично TypeScript; в случае рекурсии (прямой или косвенной) возвращаемый тип должен быть явно объявлен где-то
- поддерживаются универсальные функции
✅ Понятные и читаемые сообщения об ошибках при несоответствии типов
В FunC из-за Хиндли-Милнера ошибки несоответствия типов очень трудно понять:
error: previous function return type (int, int)
cannot be unified with implicit end-of-block return type (int, ()):
cannot unify type () with int
В языке Tolk они удобочитаемы для человека:
1) can not assign `(int, slice)` to variable of type `(int, int)`
2) can not call method for `builder` with object of type `int`
3) can not use `builder` as a boolean condition
4) missing `return`
...
✅ Тип bool
, приводящий boolVar к int
Внутри себя bool
по-прежнему -1 и 0 на уровне TVM, но с точки зрения системы типов bool
и int
теперь различны.
Операторы сравнения == / >= /...
возвращают bool
. Логические операторы && ||
возвращают bool
. Константы true
и false
имеют тип bool
.
Многие функции stdlib теперь возвращают bool
, а не int
(имея -1 и 0 во время выполнения):
var valid = isSignatureValid(...); // bool
var end = cs.isEndOfSlice(); // bool
Оператор !x
поддерживает как int
, так и bool
. Условие if
и подобные принимает как int
(!= 0), так и bool
.
Логические &&
и ||
принимают как bool
, так и int
, сохраняя совместимость с такими конструкциями, как a && b
, где a
и b
являются целыми числами (!= 0).
Арифметические операторы ограничены целыми числами, для bool разрешены только побитовые и логические:
valid && end; // ok
valid & end; // ok, bitwise & | ^ also work if both are bools
if (!end) // ok
if (~end) // error, use !end
valid + end; // error
8 & valid; // error, int & bool not allowed
Обратите внимание, что логические операторы && ||
(отсутствуют в FunC) всегда используют представление asm IF/ELSE.
В будущем для оптимизации их можно будет автоматически заменить на & |
, когда это безопасно (например: a > 0 && a < 10
).
Чтобы вручную оптимизировать потребление газа, вы по-прежнему можете использовать & |
(разрешено для bool), но помните, что они не являются сокращенными.
bool
можно привести к int
с помощью оператора as
:
var i = boolValue as int; // -1 / 0
Нет никаких преобразований во время выполнения. bool
гарантированно равен -1/0 на уровне TVM, поэтому это приведение только к типу.
Но, как правило, если вам нужно такое приведение, вероятно, вы делаете что-то неправильно (если только вы не делаете сложную побитовую оптимизацию).
✅ Универсальные функции и экземпляры, такие как f<int>(...)
В FunC были функции "forall":
forall X -> tuple tpush(tuple t, X value) asm "TPUSH";
Tolk представляет правильно сделанные универсальные функции. Их синтаксис напоминает основные языки:
fun tuplePush<T>(mutate self: tuple, value: T): void
asm "TPUSH";
Когда вызывается f<T>
, T
обнаруживается (в большинстве случаев) по предоставленным аргументам:
t.tuplePush(1); // detected T=int
t.tuplePush(cs); // detected T=slice
t.tuplePush(null); // error, need to specify "null of what type"
Также поддерживается синтаксис f<int>(...)
:
t.tuplePush<int>(1); // ok
t.tuplePush<int>(cs); // error, can not pass slice to int
t.tuplePush<int>(null); // ok, null is "null of type int"
Пользовательские функции также могут быть универсальными:
fun replaceLast<T>(mutate self: tuple, value: T) {
val size = self.tupleSize();
self.tupleSetAt(value, size - 1);
}
Вызо в replaceLast<int>
и replaceList<slice>
приведет к ДВУМ сгенерированным функциям asm (fift).
На самом деле, они в основном напоминают "шаблонные" функции. При каждом уникальном вызове тело функции полностью клонируется под новым именем.
Может быть несколько общих параметров:
fun replaceNulls<T1, T2>(tensor: (T1, T2), v1IfNull: T1, v2IfNull: T2): (T1, T2) {
var (a, b) = tensor;
return (a == null ? v1IfNull : a, b == null ? v2IfNull : b);
}
Общий параметр T
может быть чем-то сложным.
fun duplicate<T>(value: T): (T, T) {
var copy: T = value;
return (value, copy);
}
duplicate(1); // duplicate<int>
duplicate([1, cs]); // duplicate<[int, slice]>
duplicate((1, 2)); // duplicate<(int, int)>
Или даже функции, это тоже работает:
fun callAnyFn<TObj, TResult>(f: fun(TObj) -> TResult, arg: TObj) {
return f(arg);
}
fun callAnyFn2<TObj, TCallback>(f: TCallback, arg: TObj) {
return f(arg);
}
Обратите внимание, что хотя общие T
в основном определяются по аргументам, есть не столь очевидные крайние случаи, когда T
не зависит от аргументов:
fun tupleLast<T>(self: tuple): T
asm "LAST";
var last = t.tupleLast(); // error, can not deduce T
Чтобы сделать это допустимым, T
должно быть указано извне:
var last: int = t.tupleLast(); // ok, T=int
var last = t.tupleLast<int>(); // ok, T=int
var last = t.tupleLast() as int; // ok, T=int
someF(t.tupleLast()); // ok, T=(paremeter's declared type)
return t.tupleLast(); // ok if function specifies return type
Также обратите внимание, что T
для функций asm должен занимать 1 слот стека (иначе тело asm не сможет обработать его должным образом), тогда как для пользовательской функции T
может иметь любую форму.
В будущем, когда будут реализованы структуры и универсальные структуры, вся мощь универсальных функций вступит в игру.
✅ Другое наименование для recv_internal / recv_external
fun onInternalMessage
fun onExternalMessage
fun onTickTock
fun onSplitPrepare
fun onSplitInstall
Все типы параметров и их порядок переименования остаются прежними, меняется только наименование. Также доступен fun main
.
✅ #include → import. Строгий импорт
FunC | Tolk |
---|---|
#include "another.fc"; | import "another.tolk" |
В Tolk вы не можете использовать символ из a.tolk
без импорта этого файла. Другими словами, "импортируйте то, что используете".
Все функции stdlib доступны из коробки, загрузка stdlib и #include "stdlib.fc"
не требуется. См. ниже о встроенной stdlib.
По-прежнему существует глобальная область именования. Если f
объявлено в двух разных файлах, это ошибка. Мы "мпортируем" целый файл, видимость по каждому файлу отсутствует, а ключевое слово export
теперь поддерживается, но, вероятно, будет поддерживаться в будущем.
✅ #pragma → параметры компилятора
В FunC "экспериментальные" функции, такие как allow-post-modifications
, включались прагмой в файлах .fc (что приводило к проблемам, когда некоторые файлы ее содержали, а некоторые нет). На самом деле, это не прагм а для файла, а параметр компиляции.
В Tolk все прагмы были удалены. allow-post-modification
и compute-asm-ltr
были объединены в исходники Tolk (как будто они всегда были включены в FunC). Вместо прагм теперь есть возможность передавать экспериментальные параметры.
На данный момент введена одна экспериментальная опция — remove-unused-functions
, которая не включает неиспользуемые символы в вывод Fift.
#pragma version xxx
была заменена на tolk xxx
(без >=, просто строгая версия). Хорошей практикой является указание используемой версии компилятора. Если она не совпадает, Tolk выведет предупреждение.
tolk 0.6
✅ Позднее разрешение символов. Представление AST
В FunC (как и в С) вы не можете получить доступ к функции, объявленной ниже:
int b() { a(); } ;; error
int a() { ... } ;; since it's declared below
Чтобы избежать ошибки, программист должен сначала создать предварительное объявление. Причина в том, что разрешение символов выполняется прямо во время разбора.
Компилятор Tolk раз деляет эти два шага. Сначала он выполняет разбор, а затем разрешение символов. Поэтому фрагмент выше не будет ошибочным.
Звучит просто, но внутренне это очень большая работа. Чтобы сделать это доступным, я ввел промежуточное представление AST, полностью отсутствующее в FunC. Это существенный момент будущих модификаций и выполнения семантического анализа кода.
✅ ключевое слово null
Создание значений null и проверка переменных на null теперь выглядит очень красиво.
FunC | Tolk |
---|---|
a = null() | a = null |
if (null?(a)) | if (a == null) |
if (~ null?(b)) | if (b != null) |
if (~ cell_null?(c)) | if (c != null) |
Обратите внимание, что это НЕ означает, что язык Tolk допускает значение null. Нет, вы все равно можете присвоить null
целочисленной переменной — как в FunC, просто синтаксически приятно. Настоящая возможность допуска значения null будет доступна когда-нибудь, после упорной работы над системой типов.
✅ ключевые слова throw
и assert
Tolk значительно упрощает работу с исключениями.
Если в FunC есть throw()
, throw_if()
, throw_arg_if()
и то же самое для unless, то в Tolk есть только два примитива: throw
и assert
.
FunC | Tolk |
---|---|
throw(excNo) | throw excNo |
throw_arg(arg, excNo) | throw (excNo, arg) |
throw_unless(excNo, condition) | assert(condition, excNo) |
throw_if(excNo, condition) | assert(!condition, excNo) |
Обратите внимание, что !condition
возможно, так как доступно логическое НЕ, см. ниже.
Существует длинный (многословный) синтаксис assert(condition, excNo)
:
assert(condition) throw excNo;
// with possibility to include arg to throw
Кроме того, Tolk меняет местами аргументы catch
: это catch (excNo, arg)
, оба необязательны (так как arg, скорее всего, пуст).
FunC | Tolk |
---|---|
try { } catch (_, _) { } | try { } catch { } |
try { } catch (_, excNo) { } | try { } catch(excNo) { } |
try { } catch (arg, excNo) { } | try { } catch(excNo, arg) { } |
✅ do ... until
→ do ... while
FunC | Tolk |
---|---|
do { ... } until (~ condition); | do { ... } while (condition); |
do { ... } until (condition); | do { ... } while (!condition); |
Обратите внимание, что !condition
возможно, так как доступно логическое НЕ, см. ниже.
✅ Приоритет операторов стал идентичен C++ / JavaScript
В FunC такой код if (slices_equal() & status == 1)
анализируется как if( (slices_equal()&status) == 1 )
. Это является причиной различных ошибок в реальных контрактах.
В Tolk &
имеет более низкий приоритет, идентичный C++ и JavaScript.
Более того, Tolk выдает ошибки при потенциально неправильном использовании операторов, чтобы полностью исключить такие ошибки:
if (flags & 0xFF != 0)
приведет к ошибке компиляции (похоже на gcc/clang):
& has lower precedence than ==, probably this code won't work as you expected. Use parenthesis: either (... & ...) to evaluate it first, or (... == ...) to suppress this error.
Следовательно, код следует переписать:
// either to evaluate it first (our case)
if ((flags & 0xFF) != 0)
// or to emphasize the behavior (not our case here)
if (flags & (0xFF != 0))
Я также добавил диагностику для распространенной ошибки в операторах сдвига битов: a << 8 + 1
эквивалентно a << 9
, вероятно, неожиданно.
int result = a << 8 + low_mask;
error: << has lower precedence than +, probably this code won't work as you expected. Use parenthesis: either (... << ...) to evaluate it first, or (... + ...) to suppress this error.
Операторы ~% ^% /% ~/= ^/= ~%= ^%= ~>>= ^>>=
больше не существуют.
✅ Неизменяемые переменные, объявленные через val
Как в Kotlin: var
для изменяемых, val
для неизменяемых, необязательно с указанием типа. В FunC нет аналога val
.
val flags = msgBody.loadMessageFlags();
flags &= 1; // error, modifying an immutable variable
val cs: slice = c.beginParse();
cs.loadInt(32); // error, since loadInt() mutates an object
cs.preloadInt(32); // ok, it's a read-only method
Параметры функции изменяемы, но поскольку они копируются по значению, вызываемые аргументы не изменяются. Точно так же, как в FunC, просто для ясности.
fun some(x: int) {
x += 1;
}
val origX = 0;
some(origX); // origX remains 0
fun processOpIncrease(msgBody: slice) {
val flags = msgBody.loadInt(32);
...
}
processOpIncrease(msgBody); // by value, not modified
В Tolk функция может объявлять параметры mutate
. Это обобщение функций тильды FunC ~
, читайте ниже.
✅ Удалены устаревшие параметры командной строки
Флаги командной строки -A
, -P
и другие были удалены. Поведение по умолчанию
/path/to/tolk {inputFile}
более чем достаточно. Используйте -v
для печати версии и выхода. Используйте -h
для всех доступных флагов командной строки.
Можно передать только один входной файл, другие следует import
.
✅ функции stdlib переименованы в verbose понятные имена, стиль camelCase
Все наименования в стандартной библиотеке были пересмотрены. Теперь функции именуются с использованием более длинных, но понятных имен.
FunC | Tolk |
---|---|
cur_lt() | getLogicalTime() |
Бывший "stdlib.fc" был разделен на несколько файлов: common.tolk, tvm-dicts.tolk и другие.
Продолжение здесь: Tolk vs FunC: стандартная библиотека.
✅ stdlib теперь встроен, а не загружается с GitHub
FunC | Tolk |
---|---|
|
|
В Tolk stdlib — часть ди стрибутива. Стандартная библиотека неотделима, поскольку сохранение тройки "язык, компилятор, stdlib" вместе — единственный правильный способ поддерживать цикл выпуска.
Это работает таким образом. Компилятор Tolk знает, как найти стандартную библиотеку. Если пользователь установил пакет apt, исходники stdlib также были загружены и существуют на жестком диске, поэтому компилятор находит их по системным путям. Если пользователь использует оболочку WASM, они предоставляются tolk-js. И так далее.
Стандартная библиотека разделена на несколько файлов: common.tolk
(наиболее распространенные функции), gas-payments.tolk
(расчет платы за газ), tvm-dicts.tolk
и другие. Функции из common.tolk
доступны всегда (компилятор неявно импортирует их). Другие файлы необходимо импортировать явно:
import "@stdlib/tvm-dicts" // ".tolk" optional
...
var dict = createEmptyDict();
dict.iDictSet(...);
Помните правило "импортируйте то, что используете", оно также применяется к файлам @stdlib/...
(за исключением "common.tolk").
Плагин JetBrains IDE автоматически обнаруживает папку stdlib и вставляет необходимые импорты по мере ввода текста.
✅ Логические операторы && ||
, логические не !
В FunC есть только побитовые операторы ~ & | ^
. Разработчики, делающие первые шаги, думая "ладно, никаких логических, я буду использовать побитовые таким же образом", часто делают ошибки, так как поведение операторов совершенно иное:
a & b | a && b |
---|---|
иногда, идентичны: | |
0 & X = 0 | 0 & X = 0 |
-1 & X = -1 | -1 & X = -1 |
но обычно нет: | |
1 & 2 = 0 | 1 && 2 = -1 (true) |
~ found | !found |
---|---|
иногда, идентичны: | |
true (-1) → false (0) | -1 → 0 |
false (0) → true (-1) | 0 → -1 |
но обычно нет: | |
1 → -2 | 1 → 0 (false) |
условие & f() | условие && f() |
---|---|
f() вызывается всегда | f() вызывается только при выполнении условия |
условие | f() | условие || f() |
---|---|
f() вызывается всегда | f() вызывается только если условие ложно |
Tolk поддерживает логические операторы. Они ведут себя так же, как вы привыкли (правый столбец). На данный момент &&
и ||
иногда создают неоптимальный код Fift, но в будущем компилятор Tolk станет умнее в этом случае. Это незначительно, просто используйте их, как в других языках.
FunC | Tolk |
---|---|
if (~ found?) | if (!found) |
if (~ found?) { | if (!found && cs.loadInt(32) == 0) { |
ifnot (cell_null?(signatures)) | if (signatures != null) |
elseifnot (eq_checksum) | else if (!eqChecksum) |
Ключевые слова ifnot
и elseifnot
были удалены, так как теперь у нас есть логическое НЕ (для оптимизации, Компилятор Tolk генерирует IFNOTJMP
, кстати). Ключевое слово elseif
было заменено на традиционное else if
.
Помните, что логическое true
, преобразованное как int
, равно -1, а не 1. Это представление TVM.
✅ Никаких методов тильды ~
, вместо них ключевое слово mutate
Это изменение настолько велико, что описано на отдельной странице: Изменчивость Tolk.
Сравнение потребления газа в Tolk и FunC
Потребление газа Tolk может быть немного выше, поскольку оно исправляет непредвиденную перетасовку аргументов в FunC. На практике оно незначительно. В будущем компилятор Tolk станет достаточно умным, чтобы переупорядочивать аргументы, ориентируясь на меньшее количество манипуляций со стеком, но все еще избегая проблемы перетасовки.
Компилятор FunC может неожиданно перетасовать аргументы при вызове функции сборки:
some_asm_function(f1(), f2());
Иногда f2()
может быть вызвана до f1()
, и это неожиданно.
Чтобы исправить это поведение, можно указать #pragma compute-asm-ltr
, заставляя аргументы всегда вычисляться в порядке ltr.
Это было экспериментальным и поэтому отключено по умолчанию.
Эта прагма переупорядочивает аргументы в стеке, что часто приводит к большему количеству манипуляций со стеком, чем без нее. Другими словами, in исправляет неожиданное поведение, но увеличивает расход газа.
Tolk помещает аргументы в стек точно так же, как если бы эта прагма была включена. Таким образом, его потребление газа иногда выше, чем в FunC, если вы не использовали эту прагму. Конечно, в Tolk нет проблемы перемешивания.
В будущем компилятор Tolk станет достаточно умным, чтобы переупорядочивать аргументы, ориентируясь на меньшее количество манипуляций со стеком, но все еще избегая проблемы перемешивания.