跳到主要内容

Tolk vs FunC:详细介绍

下面是一份非常庞大的清单。有人有足够的耐心读到最后吗?

有一个紧凑型版本

✅ Traditional comments :)

FunCTolk
;; comment// comment
{- multiline comment -}/* multiline comment */

2+2 是 4,不是标识符。标识符只能是字母数字

在 FunC 中,几乎所有字符都可以作为标识符的一部分。 例如,2+2(不含空格)就是一个标识符。 你甚至可以用这样的名称声明一个变量。

在 Tolk 中,空格不是必须的。2+2 是 4,如所料。3+~x3 + (~ x),以此类推。

FunCTolk
return 2+2; ;; undefined function `2+2`return 2+2; // 4

更确切地说,一个标识符可以从 [a-zA-Z$_] 开始,并由 [a-zA-Z0-9$_] 继续。请注意,?: 和其他符号都不是有效的符号,found?op::increase 也不是有效的标识符。

您可以使用反标包围标识符,然后它可以包含任何符号(类似于 Kotlin 和其他一些语言)。它的潜在用途是允许将关键字用作标识符,例如在使用方案生成代码时。

FunCTolk
const op::increase = 0x1234;const OP_INCREASE = 0x1234;
;; even 2%&!2 is valid
int 2+2 = 5;
// don't do like this :)
var `2+2` = 5;

✅ 默认情况下不纯净,编译器不会放弃用户函数调用

FunC 有一个 impure 函数指定符。如果没有,函数将被视为纯函数。如果其结果未被使用,则编译器删除了其调用。

虽然这种行为已经记录在案,但对于新手来说,还是非常出乎意料。 例如,各种不返回任何内容的函数(如在不匹配时抛出异常), ,都会被默默删除。FunC 不检查和验证函数体, 允许在纯函数内部进行不纯净的操作,从而破坏了这种情况。

在 Tolk,默认所有功能都是不纯洁的。 你可以用注释标记纯函数, 然后禁止其身体中的不纯操作(异常、全局修改、 调用非纯函数等)。

✅ 新函数语法:fun 关键字、@ 属性、右侧的类型(如 TypeScript、Kotlin、Python 等)

FunCTolk
cell parse_data(slice cs) { }fun parse_data(cs: slice): cell { }
(cell, int) load_storage() { }fun load_storage(): (cell, int) { }
() main() { ... }fun main() { ... }

变量类型 - 也在右侧:

FunCTolk
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 及其他 - 带注释:

FunCTolk

int f(cell s) inline {
@inline
fun f(s: cell): int {

() load_data() impure inline_ref {
@inline_ref
fun load_data() {
global int stake_at;global stake_at: int;

forall - 是这样的:

FunCTolk
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 中,使用的是简单明了的语法:

FunCTolk
int seqno() method_id { ... }get seqno(): int { ... }

get methodName()get fun methodName() 都是可以接受的。

对于 method_id(xxx)(在实践中不常见,但有效),有一个属性:

FunCTolk

() after_code_upgrade(cont old_code) impure method_id(1666)
@method_id(1666)
fun afterCodeUpgrade(oldCode: continuation)

✅ 必须声明参数类型(尽管本地参数可有可无)

// not allowed
fun do_smth(c, n)
// types are mandatory
fun do_smth(c: cell, n: int)

有一种 auto 类型,因此 fun f(a: auto) 是有效的,但不推荐使用。

如果参数类型是强制性的,则返回类型不是(这通常是显而易见的啰嗦)。如果省略,则表示 "自动":

fun x() { ... }  // auto infer return

对于局部变量,类型也是可选的:

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 = ...;

✅ 类型系统的变化

Tolk 第一个版本中的类型系统与 FunC 中的相同,但做了以下修改:

  • void 实际上是一个空张量(命名为 unit 更规范,但 void 更可靠);另外,return(不含表达式)实际上是 return(),是从 void 函数返回的一种方便方式。
fun setContractData(c: cell): void
asm "c4 POP";
  • auto 表示 "自动推断";在 FunC 中,_ 用于此目的;注意,如果函数没有指定返回类型,它就是 auto,而不是 void
  • self,以创建可链式方法,如下所述;实际上,它不是一种类型,它只能出现在函数中,而不是函数的返回类型中
  • cont 更名为 continuation

✅ recv_internal / recv_external 的另一种命名方式

fun onInternalMessage
fun onExternalMessage
fun onTickTock
fun onSplitPrepare
fun onSplitInstall

所有参数类型及其顺序重命名不变,只是命名有所改变。fun main 也可用。

✅ #include → import.严格导入

FunCTolk
#include "another.fc";import "another.tolk"

在 Tolk 中,如果不导入该文件,就无法使用 a.tolk 中的符号。换句话说,就是 用什么导入什么

所有 stdlib 函数开箱即用,无需下载 stdlib 和 #include "stdlib.fc"。有关嵌入式 stdlib,请参阅下文。

命名仍有全局范围。如果 f 在两个不同的文件中声明,就会出错。我们 "导入 "的是整个文件,而不是每个文件的可见性,export 关键字现在还不支持,但将来可能会支持。

✅ #pragma → 编译器选项

在 FunC 中,"允许事后修改"(allow-post-modifications)等 "试验性 "功能是通过 .fc 文件中的一个 pragma 打开的(导致有些文件包含,有些不包含的问题)。事实上,这不是文件的 pragma,而是编译选项。

在 Tolk 中,所有实用程序都被移除。allow-post-modificationcompute-asm-ltr 被合并到 Tolk 源中(就像它们在 FunC 中一直处于开启状态一样)。现在可以传递实验选项来代替语法标记。

目前,我们引入了一个实验性选项-- remove-unused-functions(删除未使用的函数),它不会将未使用的符号包含到 Fift 输出中。

#pragma version xxxtolk xxx 代替(没有 >=,只有严格版本)。注释您正在使用的编译器版本是一个很好的做法。如果不匹配,Tolk 会发出警告。

tolk 0.6

✅ 后期符号解析。AST 表示

在 FunC 中(如在 С 中),不能访问下面声明的函数:

int b() { a(); }   ;; error
int a() { ... } ;; since it's declared below

为避免出错,程序员应首先创建一个正向声明。因为符号解析是在解析时进行的。

Tolk 编译器将这两个步骤分开。首先是解析,然后是符号解析。因此,上述代码段不会出错。

听起来很简单,但在内部却是一项非常艰巨的工作。为了实现这一点,我引入了 FunC 完全没有的中间 AST 表示法。这是未来修改和执行语义代码分析的关键点。

null 关键字

创建空值和检查变量是否为空现在看起来非常漂亮。

FunCTolk
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 --就像在 FunC 中一样,只是在语法上更友好而已。经过对类型系统的努力,真正的可空性总有一天会实现。

throwassert 关键字

Tolk 大大简化了处理异常的工作。

如果 FunC 有 throw()throw_if()throw_arg_if(),unless 也一样,那么 Tolk 就只有两个原语:throwassert

FunCTolk
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 是可能的,因为逻辑 NOT 可用,见下文。

assert(condition,excNo) 语法较长(冗长):

assert(condition) throw excNo;
// with possibility to include arg to throw

此外,Tolk 交换了 catch 参数:它是 catch (excNo, arg),两个参数都是可选的(因为 arg 很可能是空的)。

FunCTolk
try { } catch (_, _) { }try { } catch { }
try { } catch (_, excNo) { }try { } catch(excNo) { }
try { } catch (arg, excNo) { }try { } catch(excNo, arg) { }

do ... untildo ... while

FunCTolk
do { ... } until (~ condition);do { ... } while (condition);
do { ... } until (condition);do { ... } while (!condition);

注意,!condition 是可能的,因为逻辑 NOT 可用,见下文。

✅ 运算符优先级变得与 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 ~ tilda 函数的概括,请阅读下文。

✅ 删除过时的命令行选项

删除了命令行标志 -A-P 和其他标志。默认行为

/path/to/tolk {inputFile}

就足够了。使用 -v 打印版本并退出。使用 -h 查看所有可用的命令行标志。

只能传递一个输入文件,其他文件应 import

✅ stdlib 函数重命名为 verbose 清晰名称,驼峰式

重新考虑了标准库中的所有命名。现在,函数的命名更长但更清晰。

FunCTolk
cur_lt()
car(l)
get_balance().pair_first()
raw_reserve(count)
dict~idict_add?(...)
dict~udict::delete_get_max()
t~tpush(triple(x, y, z))
s.slice_bits()
~dump(x)
...
getLogicalTime()
listGetHead(l)
getMyOriginalBalance()
reserveToncoinsOnBalance(count)
dict.iDictSetIfNotExists(...)
dict.uDictDeleteLastAndGet()
t.tuplePush([x, y, z])
s.getRemainingBitsCount()
debugPrint(x)
...

以前的 "stdlib.fc "被拆分成多个文件:common.tlk、tvm-dicts.tlk 和其他文件。

继续此处:Tolk vs FunC:标准库

✅ stdlib 现在是嵌入式的,而不是从 GitHub 下载

FunCTolk
  1. Download stdlib.fc from GitHub
  2. Save into your project
  3. #include "stdlib.fc";
  4. Use standard functions
  1. Use standard functions

在 Tolk 中,stdlib 是发行版的一部分。标准库是不可分割的,因为将 语言、编译器、stdlib 三者保持在一起是保持发布周期的唯一正确方法。

它是这样工作的。Tolk 编译器知道如何定位标准库。如果用户安装了 apt 软件包,stdlib 源也会被下载并存在硬盘上,因此编译器会通过系统路径找到它们。如果用户使用的是 WASM 封装器,则由 tolk-js 提供。以此类推。

标准库分为多个文件:common.tolk(最常用的函数)、gas-payments.tolk(计算 gas 费)、tvm-dicts.tolk 和其他文件。common.tolk 中的函数始终可用(编译器会隐式导入)。其他文件则需要明确导入:

import "@stdlib/tvm-dicts"   // ".tolk" optional

...
var dict = createEmptyDict();
dict.iDictSet(...);

注意 "用什么导入什么" 的规则,它也适用于 @stdlib/... 文件("common.tolk "是唯一的例外)。

JetBrains IDE 插件会自动发现 stdlib 文件夹,并在输入时插入必要的导入。

✅ 逻辑运算符 && ||, 逻辑非 !

在 FunC 中,只有位运算符 ~ & | ^。开发人员在进行第一步开发时,如果认为 "好吧,没有逻辑,我就用同样的方式使用位运算符",往往会出错,因为运算符的行为是完全不同的:

a & ba && b
sometimes, identical:
0 & X = 00 & X = 0
-1 & X = -1-1 & X = -1
but generally, not:
1 & 2 = 01 && 2 = -1 (true)
~ found!found
sometimes, identical:
true (-1) → false (0)-1 → 0
false (0) → true (-1)0 → -1
but generally, not:
1 → -21 → 0 (false)
condition & f()condition && f()
f() is called alwaysf() is called only if condition
condition | f()condition || f()
f() is called alwaysf() is called only if condition is false

Tolk 支持逻辑运算符。它们的行为与您习惯的完全一样(右列)。目前,&&|| 有时会产生不理想的 Fift 代码,但将来 Tolk 编译器在这种情况下会变得更聪明。这可以忽略不计,只需像使用其他语言一样使用它们即可。

FunCTolk
if (~ found?)if (!found)
if (~ found?) {
    if (cs~load_int(32) == 0) {
        ...
    }
}
if (!found && cs.loadInt(32) == 0) {
    ...
}
ifnot (cell_null?(signatures))if (signatures != null)
elseifnot (eq_checksum)else if (!eqChecksum)

删除了关键字 ifnotelseifnot ,因为现在我们有了逻辑 not(为了优化,Tolk 编译器会生成 IFNOTJMP)。关键字 elseif 被传统的 else if 取代。

请注意,这并不意味着 Tolk 语言具有 bool 类型。不,比较运算符仍然返回整数。经过对类型系统的努力,总有一天会支持 bool 类型。

请记住,true 是-1,而不是 1。在 FunC 和 Tolk 中都是如此。这是一种 TVM 表示法。

✅ 不使用 ~ 方法,改用 mutate 关键字

这一改动非常巨大,因此在单独的页面上进行了描述:Tolk 突变性


Tolk vs FunC gas 消耗量

TLDR

Tolk 的耗气量可能会更高一些,因为它可以解决 FunC 中意外的争论洗牌问题。实际上,这可以忽略不计。
将来,Tolk 编译器会变得足够聪明,可以对参数重新排序,减少堆栈操作, ,但仍能避免洗牌问题。

在调用汇编函数时,FunC 编译器可能会意外更改参数:

some_asm_function(f1(), f2());

有时,f2() 可能会在 f1() 之前被调用,这是意料之外的。 要解决这个问题,可以指定 #pragma compute-asm-ltr,强制参数总是按 ltr 顺序求值。 这只是试验性的,因此默认关闭。

这个 pragma 会对堆栈中的参数重新排序,通常会导致比不使用它时更多的堆栈操作。 换句话说,它可以修复意外行为,但会增加耗气量。

Tolk 将参数放入堆栈的方式与开启此实用程序完全相同。 因此,如果不使用该实用程序,其耗气量有时会高于 FunC。 当然,在 Tolk 中不存在洗牌问题。

未来,Tolk 编译器将变得足够智能,可以对参数重新排序,减少堆栈操作, ,但仍能避免洗牌问题。