跳到主要内容

语句

本节简要讨论构成普通函数体代码的 FunC 语句。

表达式语句

最常见的语句类型是表达式语句。它是一个表达式后跟 ;。表达式的描述相当复杂,因此这里只提供一个概述。通常所有子表达式都是从左到右计算的,唯一的例外是汇编堆栈重排,它可能手动定义顺序。

变量声明

不可能声明一个局部变量而不定义其初始值。

以下是一些变量声明的示例:

int x = 2;
var x = 2;
(int, int) p = (1, 2);
(int, var) p = (1, 2);
(int, int, int) (x, y, z) = (1, 2, 3);
(int x, int y, int z) = (1, 2, 3);
var (x, y, z) = (1, 2, 3);
(int x = 1, int y = 2, int z = 3);
[int, int, int] [x, y, z] = [1, 2, 3];
[int x, int y, int z] = [1, 2, 3];
var [x, y, z] = [1, 2, 3];

变量可以在同一作用域内“重新声明”。例如,以下是正确的代码:

int x = 2;
int y = x + 1;
int x = 3;

事实上,int x 的第二次出现不是声明,而只是编译时确认 x 的类型为 int。因此第三行实际上等同于简单的赋值 x = 3;

在嵌套作用域中,变量可以像在 C 语言中一样真正重新声明。例如,考虑以下代码:

int x = 0;
int i = 0;
while (i < 10) {
(int, int) x = (i, i + 1);
;; 这里 x 是类型为 (int, int) 的变量
i += 1;
}
;; 这里 x 是类型为 int 的(不同)变量

但如在全局变量章节中提到的,不允许重新声明全局变量。

请注意,变量声明表达式语句,因此像 int x = 2 这样的结构实际上是完整的表达式。例如,以下是正确的代码:

int y = (int x = 3) + 1;

它声明了两个变量 xy,分别等于 34

下划线

下划线 _ 用于表示不需要的值。例如,假设函数 foo 的类型为 int -> (int, int, int)。我们可以获取第一个返回值并忽略第二个和第三个,如下所示:

(int fst, _, _) = foo(42);

函数应用

函数调用看起来像在常规语言中那样。函数调用的参数在函数名之后列出,用逗号分隔。

;; 假设 foo 的类型为 (int, int, int) -> int
int x = foo(1, 2, 3);

但请注意,foo 实际上是一个参数类型为 (int, int, int) 的函数。为了看到区别,假设 bar 是类型为 int -> (int, int, int) 的函数。与常规语言不同,你可以这样组合函数:

int x = foo(bar(42));

而不是类似但更长的形式:

(int a, int b, int c) = bar(42);
int x =

foo(a, b, c);

也可以进行 Haskell 类型的调用,但不总是可行(稍后修复):

;; 假设 foo 的类型为 int -> int -> int -> int
;; 即它是柯里化的
(int a, int b, int c) = (1, 2, 3);
int x = foo a b c; ;; ok
;; int y = foo 1 2 3; 不会编译
int y = foo (1) (2) (3); ;; ok

Lambda 表达式

暂不支持 Lambda 表达式。

方法调用

非修改方法

如果函数至少有一个参数,它可以作为非修改方法调用。例如,store_uint 的类型为 (builder, int, int) -> builder(第二个参数是要存储的值,第三个是位长度)。begin_cell 是创建新构建器的函数。以下代码是等价的:

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 是某种任意类型,那么该函数可以作为修改方法调用。修改方法调用可以接受一些参数并返回一些值,但它们会修改其第一个参数,即将返回值的第一个组件赋值给第一个参数的变量。例如,假设 cs 是一个cell切片,load_uint 的类型为 (slice, int) -> (slice, int):它接受一个cell切片和要加载的位数,并返回切片的剩余部分和加载的值。以下代码是等价的:

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

在某些情况下,我们希望将不返回任何值且只修改第一个参数的函数用作修改方法。可以使用cell类型如下操作:假设我们想定义类型为 int -> int 的函数 inc,它用于递增整数,并将其用作修改方法。然后我们应该将 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 的函数时,如果存在 .foo~foo 的定义,FunC 编译器将分别使用 .foo~foo 的定义,如果没有,则使用 foo 的定义。

运算符

请注意,目前所有的一元和二元运算符都是整数运算符。逻辑运算符表示为位整数运算符(参见没有布尔类型)。

一元运算符

有两个一元运算符:

  • ~ 是按位非(优先级 75)
  • - 是整数取反(优先级 20)

它们应该与参数分开:

  • - x 是可以的。
  • -x 不可以(它是单个标识符)

二元运算符

优先级为 30(左结合性):

  • * 是整数乘法
  • / 是整数除法(向下取整)
  • ~/ 是整数除法(四舍五入)
  • ^/ 是整数除法(向上取整)
  • % 是整数取模运算(向下取整)
  • ~% 是整数取模运算(四舍五入)
  • ^% 是整数取模运算(向上取整)
  • /% 返回商和余数
  • & 是按位与

优先级为 20(左结合性):

  • + 是整数加法
  • - 是整数减法
  • | 是按位或
  • ^ 是按位异或

优先级为 17(左结合性):

  • << 是按位左移
  • >> 是按位右移
  • ~>> 是按位右移(四舍五入)
  • ^>> 是按位右移(向上取整)

优先级为 15(左结合性):

  • == 是整数等值检查
  • != 是整数不等检查
  • < 是整数比较
  • <= 是整数比较
  • > 是整数比较
  • >= 是整数比较
  • <=> 是整数比较(返回 -1、0 或 1)

它们也应该与参数分开:

  • x + y 是可以的
  • x+y 不可以(它是单个标识符)

条件运算符

它具有通常的语法。

<condition> ? <consequence> : <alternative>

例如:

x > 0 ? x * fac(x - 1) : 1;

优先级为 13。

赋值

优先级 10。

简单赋值 = 以及二元运算的对应项:+=-=*=/=~/=^/=%=~%=^%=<<=>>=~>>=^>>=&=|=^=

循环

FunC 支持 repeatwhiledo { ... } until 循环。不支持 for 循环。

Repeat 循环

语法是 repeat 关键字后跟一个类型为 int 的表达式。指定次数重复代码。示例:

int x = 1;
repeat(10) {
x *= 2;
}
;; x = 1024
int x = 1, y = 10;
repeat(y + 6) {
x *= 2;
}
;; x = 65536
int x = 1;
repeat(-1) {
x *= 2;
}
;; x = 1

如果次数小于 -2^31 或大于 2^31 - 1,将抛出范围检查异常。

While 循环

具有通常的语法。示例:

int x = 2;
while (x < 100) {
x = x * x;
}
;; x = 256

请注意,条件 x < 100 的真值是类型为 int 的(参见没有布尔类型)。

Until 循环

具有以下语法:

int x = 0;
do {
x += 3;
} until (x % 17 == 0);
;; x = 51

If 语句

示例:

;; 通常的 if
if (flag) {
do_something();
}
;; 等同于 if (~ flag)
ifnot (flag) {
do_something();
}
;; 通常的 if-else
if (flag) {
do_something();
}
else {
do_alternative();
}
;; 一些特定功能
if (flag1) {
do_something1();
} else {
do_alternative4();
}

花括号是必需的。以下代码将无法编译:

if (flag1)
do_something();

Try-Catch 语句

自 func v0.4.0 起可用

执行 try 块中的代码。如果失败,完全回滚在 try 块中所做的更改,并执行 catch 块;catch 接收两个参数:任何类型的异常参数(x)和错误代码(n,整数)。

与许多其他语言不同,在 FunC 的 try-catch 语句中,try 块中所做的更改,特别是局部和全局变量的修改,所有寄存器的更改(即 c4 存储寄存器、c5 操作/消息寄存器、c7 上下文寄存器等)被丢弃,如果 try 块中有错误,因此所有合约存储更新和消息发送将被撤销。需要注意的是,一些 TVM 状态参数,如 codepage 和gas计数器不会回滚。这意味着,尤其是,try 块中花费的所有gas将被计入,以及改变gas限制的操作(accept_messageset_gas_limit)的效果将被保留。

请注意,异常参数可以是任何类型(可能在不同异常情况下不同),因此 funC 无法在编译时预测它。这意味着开发者需要通过将异常参数转换为某种类型来“帮助”编译器(请参见下面的示例 2):

示例:

try {
do_something();
} catch (x, n) {
handle_exception();
}
forall X -> int cast_to_int(X x) asm "NOP";
...
try {
throw_arg(-1, 100);
} catch (x, n) {
x.cast_to_int();
;; x = -1, n = 100
return x + 1;
}
int x = 0;
try {
x += 1;
throw(100);
} catch (_, _) {
}
;; x = 0(而非 1)

区块语句

也允许使用区块语句。它们打开一个新的嵌套作用域:

int x = 1;
builder b = begin_cell();
{
builder x = begin_cell().store_uint(0, 8);
b = x;
}
x += 1;