系列文章目录
文章目录
前言
在上文中我们详细介绍了toncli的编译命令,这次我们将深入底层来剖析一下,一个空的func合约是在做什么
一、func编译到fift
我们使用func -w main.func -o output.fif 编译下面的文件
() main(int msg_value, cell in_msg, slice in_msg_body) impure {
}
得到
"Asm.fif" include
// automatically generated from `/home/zqy/tonlearn/my_first_contract/contracts/main.fc`
PROGRAM{
DECLPROC main
main PROC:<{
3 BLKDROP
}>
}END>c
二、详细分析fift文件
1. PROC指令
因为fift没有成熟的vscode拓展,所以需要我们将asm.fif和FIFT.fif添加到根目录下,搜索解析,博主手动解析如下
{ 1000 @def-proc } : PROC
{ // i s l
dup 0< {
negate
@was-split @ { drop 0 } if
} if
@adj-long-proc over @procdict @ @procdictkeylen
idict!+ not abort"cannot define procedure, redefined?"
@procdict ! 2 2 @procinfo~!
}
variable @was-split
variable @procdict
variable @procinfo
{ over sbits < { s>c <b swap ref, b> <s } if } : @adj-long-proc
sbits (s - x), returns the number of data bits x remaining in Slice s,
19 constant @procdictkeylen
{ not 2 pick @ and xor swap ! } : ~!
idict! (v x D n – D′ −1 or D 0), adds a new value v (represented by a
Slice) with key given by signed big-endian n-bit integer x into dictionary
D (represented by a Cell or a Null ) with n-bit keys, and returns the new
dictionary D′ and −1 on success. Otherwise the unchanged dictionary
D and 0 are returned
if (x e – ), executes e (which must be an execution token, i.e., a
WordDef ),5 but only if Integer x is non-zero
这段代码是 Fift 语言的一部分,它定义了一个简单的 Fift 解释器中的一些核心词(words),这些词是 Fift 脚本的基础,用于定义和操作程序字典(procedure dictionary)。
定义 PROC
词
{ 1000 @def-proc } : PROC
这行代码定义了一个名为 PROC
的词(word),它是一个工厂词(factory word),用于创建新的词。这里的 { 1000 @def-proc }
是一个代码块(colon definition),它调用了 @def-proc
词,并传入 1000
作为参数。@def-proc
词的作用是定义一个新的词,并将其存储在程序字典中。
定义 @def-proc
词
{ // i s l
dup 0< {
negate
@was-split @ { drop 0 } if
} if
@adj-long-proc over @procdict @ @procdictkeylen
idict!+ not abort"cannot define procedure, redefined?"
@procdict ! 2 2 @procinfo~!
} variable @was-split
variable @procdict
variable @procinfo
这部分代码定义了 @def-proc
词,它接受三个参数:i
(一个整数),s
(一个字符串),和 l
(一个切片)。这个代码块执行以下操作:
-
dup 0<
:复制i
并检查它是否小于 0。这意味着,我们传入的总是会变成无符号整数,或绝对值。记住这里的算法。在 fift 语言中,if
语句是一种控制流结构,用于根据条件执行不同的代码路径。 -
if
:这是 Fift 中的条件语句关键字,用于引入一个条件判断。(x e – )
:这是 Fift 语法中的条件执行结构。括号(...)
用于将多个操作组合在一起。在这个结构中:-
x
:这是一个整数值,用作条件判断。如果x
非零,条件判断为真;如果x
为零,条件判断为假。 -
e
:这是一个执行令牌(execution token),通常是一个词(word)的定义,例如WordDef
。在 Fift 中,一个词的定义(WordDef
)是一个特殊的数据结构,它包含了词的名称、代码块和其他元数据。 -
–
:这个符号在 Fift 中表示“丢弃并执行”的操作。它用于指示 Fift 解释器忽略x
的值,并执行e
指定的代码。 -
executes e
:这意味着如果条件x
非零,Fift 解释器将执行e
指定的代码。如果x
为零,则跳过e
指定的代码。 -
only if Integer x is non-zero
:这是一个条件,指定只有当x
为非零整数时,才会执行e
。如果x
为零,则不会执行e
。
-
假设我们有一个 Fift 词 my-word
,我们想在某个整数 x
非零时执行它。我们可以这样写:
: my-word
." Hello, World! " cr
;
0 my-word if
在这个例子中,my-word
将不会被执行,因为 if
语句的条件 0
是假的。
如果我们将条件改为一个非零值:
: my-word
." Hello, World! " cr
;
1 my-word if
这次,my-word
将被执行,因为 if
语句的条件 1
是真的。
dup bool {e} if //{e}被执行当且仅当,bool是true , 也就是-1。
negate
:如果i
小于 0,则取其相反数。@was-split @ { drop 0 } if
:如果@was-split
变量的值非零,则丢弃它并设置为 0。PS(这里的意思就是不管是啥都按照零来算。但其实有些多此一举。完全可以不使用这个变量。)
@was-split @ //这里是很常用的方法@是为了取出变量当中的值
@adj-long-proc
:调整长整数的处理。over @procdict @ @procdictkeylen
:获取程序字典和键长度。idict!+
:将新的词添加到程序字典中。如果添加失败(即词已存在),则返回 0。abort"cannot define procedure, redefined?"
:如果词已存在,则抛出错误。@procdict !
:将新的程序字典存储在@procdict
变量中。2 2 @procinfo~!
:更新@procinfo
变量。
3. 定义 @adj-long-proc
词
{ over sbits < { s>c <b swap ref, b> <s } if } : @adj-long-proc
这个代码块定义了 @adj-long-proc
词,它接受两个参数:over
(一个切片)和 sbits
(一个整数)。
4. 定义 sbits
词
sbits (s - x), returns the number of data bits x remaining in Slice s,
在 Fift 语言中,sbits
是一个用于操作切片(slice)的内置词(word)。切片是 Fift 和其他 Forth 语言中用于表示数据片段的一种数据结构,它可以包含任意长度的二进制数据。
sbits
这个词的作用是返回切片中剩余的位数(bits)。在 Fift 中,切片通常用于处理数据,比如智能合约的代码或者数据,因此知道切片中剩余多少位数据是非常重要的。使用 ``sbits` 的使用非常简单,它通常接受一个切片作为参数,并返回该切片中剩余的位数。例如:
my-slice sbits . cr
这里,my-slice
是一个切片变量,sbits
被用来获取这个切片中的剩余位数,并通过 . cr
打印出来。.
是打印栈顶元素的 Fift 词,而 cr
是换行的 Fift 词。 sbits
的内部工作
在 Fift 的实现中,切片通常由两个主要部分组成:元数据和数据本身。元数据包含了切片的大小、容量等信息,而数据部分则是实际的二进制数据。
当你对一个切片使用 sbits
时,Fift 解释器会查看切片的元数据,确定切片中已经使用了多少位,然后从切片的总大小中减去这个数值,得到剩余的位数。
sbits
返回的是切片中剩余的位数,而不是字节数。如果你需要字节数,可能需要将结果除以 8。- 在使用
sbits
时,确保你了解切片的当前状态,比如它是否已经被修改过,或者是否包含了预期的数据。sbits
是 Fift 语言中处理切片数据时的一个有用工具,它提供了一种快速检查切片中剩余数据量的方法。
这个代码块执行以下操作:
over sbits <
:检查切片中的位数是否小于sbits
。s>c <b swap ref, b> <s
:如果小于,则将切片转换为字符,执行某些操作,然后转换回切片。
在 Fift 语言中,s>c
是一个将切片(Slice)转换为单元(Cell)的内置词(word)。切片和单元是 Fift 中处理数据的两种基本数据结构,它们在 TON 区块链中也扮演着重要的角色。
5 s>c
词的解释
s>c
:这个操作接受一个切片(Slice)作为输入,并创建一个新的单元(Cell)c
,该单元直接包含切片的数据。(s – c)
:这表示s>c
词的栈效应,即它从栈中弹出一个切片s
并返回一个新的单元c
。<b
:这是一个编译时操作,它开始一个新的字节序列的编译。swap s,
:这个操作首先将切片s
压入栈中,然后交换栈顶的两个元素,使得字节序列编译器和切片s
相邻。b>
:这个操作结束字节序列的编译,并将编译后的字节序列转换为一个新的单元。
所以,s>c
的等效操作可以写成:
<b swap s, b>
这个序列首先开始编译一个新的字节序列,然后将切片 s
压入栈中并交换位置,最后结束编译并创建一个新的单元。
s>c
词通常用于以下场景:
- 当你需要将切片中的数据转换为单元时,这在处理复杂的数据结构或者在将数据存储到区块链上时非常有用。
- 当你需要在切片和单元之间进行转换,以便利用 Fift 提供的各种单元操作词来进一步处理数据。
一个切片 my-slice
,将其转换为单元 my-cell
,可以这样写:
my-slice s>c my-cell
这行代码将创建一个新的单元 my-cell
,它包含了 my-slice
中的数据。
- 当使用
s>c
时,确保切片中的数据是有效的,因为单元一旦创建,其内容将与切片相同,且不可更改。 s>c
操作不会修改原始切片,它只是创建了一个新的单元。
6. 定义 @procdictkeylen
常量
19 constant @procdictkeylen
这行代码定义了一个常量 @procdictkeylen
,其值为 19。这个常量用于指定程序字典中的键长度。
7. 定义 ~!
词
{ not 2 pick @ and xor swap ! } : ~!
在 Fift 语言中,~!
是一个操作符,用于执行按位取反(bitwise NOT)操作后,将结果存储回原变量。这个操作符结合了按位取反操作和赋值操作。
-
~!
:这是一个双目操作符,意味着它接受两个操作数。 -
第一个操作数通常是一个字面量或变量,它将被按位取反。
-
第二个操作数是一个变量,其值将被更新为第一个操作数的按位取反结果。
-
( x – )
:~!
操作符从栈中弹出一个值x
,执行按位取反操作,但不返回任何值。
- 取反操作:
~
是按位取反操作符,它将操作数的每个位取反。例如,如果操作数是0011 1010
(二进制表示),取反后将变为1100 0101
。 - 赋值操作:
!
是赋值操作符,它将取反后的结果存回原变量。
假设我们有一个变量 flag
,其值为 10
(二进制表示为 1010
),我们想对其执行按位取反操作:
10 flag ~!
执行这行代码后,flag
的值将变为 0101
(十进制表示为 5
)。
9 词定义:idict!
idict!
:这个词的作用是将一个值v
(一个切片)与一个整数键x
一起添加到字典D
中。(v x D n – D′ −1 or D 0)
:这是idict!
的栈效应,描述了操作前后栈的变化。v
:要添加到字典中的值,通常是一个切片(Slice)。x
:与值v
关联的键,是一个有符号的大端整数。D
:要添加键值对的字典,可以是一个单元(Cell)或者空值(Null)。n
:键x
的位数,表示键是一个n
位的整数。D′
:如果操作成功,D′
是更新后的字典。-1
:如果操作成功,返回-1
作为成功的标志。0
:如果操作失败(例如,键已存在),返回0
。
- 参数入栈:首先,将值
v
、键x
、字典D
和键的位数n
压入栈中。 - 检查字典:检查
D
是否为空。如果D
为空,说明字典尚未初始化,可能需要先创建一个新的字典。 - 添加键值对:将值
v
和键x
添加到字典D
中。这个操作会检查键x
是否已经存在于字典中。- 如果键不存在,将其添加到字典中,并返回更新后的字典
D′
和-1
作为成功的标志。 - 如果键已存在,操作失败,返回原始字典
D
和0
。
- 如果键不存在,将其添加到字典中,并返回更新后的字典
- 返回结果:操作完成后,根据操作的成功与否,返回相应的值。
假设我们有一个字典 my-dict
,我们想添加一个键值对,其中键是一个 32 位的整数 0x00000001
,值是一个切片 my-slice
:
my-slice 1 32 my-dict idict!
这行代码将尝试将键 0x00000001
和值 my-slice
添加到字典 my-dict
中。如果添加成功,my-dict
将被更新,并且栈上将留下 -1
。如果键已存在,my-dict
将保持不变,并且栈上将留下 0
。
idict!
操作是幂等的,意味着多次添加相同的键值对不会改变字典的状态。- 在使用
idict!
之前,确保字典D
已经初始化,否则可能需要先创建一个新的字典。 - 操作的成功与否取决于键是否已经存在于字典中,这可以通过返回值
-1
或0
来判断。
idict!
是 Fift 语言中处理字典数据结构的一个强大工具,它允许开发者在字典中存储和管理键值对。
总结
在 Fift 语言中,PROC
是一个工厂词(factory word),它用于创建新的词(word)。在 Forth 类语言中,词是基本的编程构建块,类似于其他语言中的函数或子程序。PROC
提供了一种简便的方式来定义新的词,同时处理与词创建相关的内部细节,如管理程序字典和确保词的唯一性。
-
封装词创建过程:
PROC
隐藏了创建新词的复杂性,提供了一个简单的接口来定义新词。用户只需要提供词的行为(即执行体),而PROC
负责将这个行为注册到程序字典中。 -
管理程序字典:程序字典是一个数据结构,它存储了所有已定义的词及其关联的执行令牌。
PROC
通过idict!
词来管理这个字典,确保每个新词都能被正确地添加到字典中。 -
确保词的唯一性:在添加新词到程序字典时,
PROC
会检查这个词是否已经存在。如果词已存在,PROC
会抛出错误,防止意外地覆盖已有的词。 -
处理词的属性:
PROC
可以接受额外的参数来定义词的属性,比如是否是立即执行的(immediate)或者是否可编译的(compile-only)。这些属性影响词在 Fift 程序中的使用方式。 -
简化错误处理:通过
PROC
定义的词,如果词的定义过程中出现错误,PROC
可以提供更友好的错误信息,帮助开发者快速定位问题。 -
支持词的重载:在某些情况下,你可能需要定义多个具有相同名称但不同行为的词。
PROC
可以通过管理不同的执行令牌来支持这种重载机制。 -
提供默认行为:
PROC
可以接受默认的执行体,这样即使在没有提供具体行为的情况下,也可以创建一个词的框架,稍后再填充具体的行为。
在 Fift 中定义词(word)时,涉及到的中间值比较和处理,比如 1000
、19
、“字典”、“小于零绝对值”等,都是确保词正确定义和存储的关键步骤。下面是这些步骤的详细解释:
1000
的作用
在代码 { 1000 @def-proc } : PROC
中,1000
是一个示例参数,它被传递给 @def-proc
词。这个参数可能表示词的属性或特定的行为,比如词的权限级别、可见性或其他元数据。在不同的上下文中,这个值可以有不同的含义,但通常它用于配置词创建过程中的某些方面。
19
的作用
19
在代码 19 constant @procdictkeylen
中定义了一个常量 @procdictkeylen
,它指定了程序字典中的键长度为 19 位。这意味着程序字典的键是一个 19 位的有符号大端整数。这个长度的选择可能基于 Fift 运行时的内部设计,用于确保键的唯一性和足够的存储空间。
字典的作用
在 Fift 中,字典(dictionary)是一种数据结构,用于存储词的定义。每个词都有一个唯一的键(通常是一个整数),和一个值(通常是包含词代码的切片)。字典允许快速查找和执行词,是 Fift 虚拟机的核心组件。
- 程序字典:存储了所有词的定义,每个词都可以通过其键快速访问。
- 更新字典:当新词被定义时,
idict!
词用于将新词添加到程序字典中。如果键已存在,则表示词已被定义,idict!
将返回失败。
小于零绝对值的作用
在代码 dup 0< { negate } if
中,dup 0<
检查某个值是否小于零,如果是,则 negate
将其转换为正数。这个步骤确保了即使输入的是负数,处理的也是其绝对值。在词定义的上下文中,这可能用于确保键始终是正数,因为字典的键通常不能是负数。
这些中间值比较和处理步骤在 Fift 中定义词时起到了以下作用:
- 确保键的正数性:通过将负数转换为正数,确保字典的键始终是正数。
- 配置词属性:通过传递参数(如
1000
)给@def-proc
,可以配置新词的属性。 - 管理字典键长度:通过设置
@procdictkeylen
,定义了字典键的长度,确保键的唯一性和存储空间。 - 确保词的唯一性:通过检查字典中是否已存在键,防止词的重复定义。