翻译原文
date:20170724
Solidity定义一种汇编语言,可脱离Solidity使用。该汇编语言可以被用来“内联编译”。我们开始于描述如何使用内联编译,它和独立编译的区别,最后阐述汇编。(?Solidity defines an assembly language that can also be used without Solidity. This assembly language can also be used as “inline assembly” inside Solidity source code. We start with describing how to use inline assembly and how it differs from standalone assembly and then specify assembly itself.)
//TODO:这里还要写,行内编译的作用域有点区别,调用库的内部函数时编译过程。更进一步,写写编译器定义的符号。
内联汇编
为了更细粒度的控制,尤其是通过写库来加强语言,可以在语言上引入内联汇编与虚拟机语言相交错。(?For more fine-grained control especially in order to enhance the language by writing libraries, it is possible to interleave Solidity statements with inline assembly in a language close to the one of the virtual machine.)由于EVM是堆栈机,所以很难在堆栈中寻址正确的堆栈片和在正确的时间点为操作码提供正确的操作数。Solidity内联编译尝试加强这个缺陷和其他通过以下命令手动编译出现的问题:
- 函数类型的操作码:
mul(1, add(2,3))
来替换push1 3 push1 2 add push1 1 mul
- 汇编变量:
let x:= add(2, 3) let y := mload(0x40) x := add(x,y)
- 访问外部变量:
function f(uint x) { assembly { x := sub(x, 1) } }
- 标签:
let x:= 10 repeat: x := sub(x, 1) jumpi(repeat,eq(x, 0))
- 循环:
for { let i := 0} lt(i, x) { i := add(i, 1) } { y := mul(2, y)}
- switch表达式:
switch x case 0 {y := mul(x, 2)} default { y:= 0}
- 函数调用:
function f(x) -> y {switch x case 0 { y := 1} default {y := mul(x, f(sub(x, 1)))}}
现在我们将详细描述行内编译语言。
警告:行内编译是访问EVM底层的一种方式。这损失了Solidity的几个安全特性。
例子
下面的例子提供了访问其他合约的库函数的代码,并将它赋值到bytes
变量。完全solidity是不可能的,现在的理念是汇编库用来加强语言。
pragma solidity ^0.4.0;
library GetCode {
function at(address _addr) returns (bytes o_code) {
assembly {
// 获取代码的大小,这需要汇编
let size := extcodesize(_addr)
// 分配输出数组 - 这可以不需要汇编就能完成
// 通过代码 using o_code = new bytes(size)
o_code := mload(0x40)
//新的 "内存端点" 包含填充
mstore(0x40, add(o_code, and(add(add(size, 0x20), 0x1f), not(0x1f))))
// 内存中的存储长度
mstore(o_code, size)
// 获取代码, 这需要汇编
extcodecopy(_addr, add(o_code, 0x20), 0, size)
}
}
}
在优化器无法提高代码效率的时候,内联汇编就会显得非常有用。请注意汇编比较难写,因为编译器不会检查代码,所以你在明白自己在做什么的时候才可以使用。
pragma solidity ^0.4.0;
library VectorSum {
// 这个函数由于优化器不能对数组进行边界检查,所以比较低效。
function sumSolidity(uint[] _data) returns (uint o_sum) {
for (uint i = 0; i < _data.length; ++i)
o_sum += _data[i];
}
// 我们知道我们只在一定范围内访问数组,所以我们避免检查边界。
// 0x20 需要添加到数组,因为第一个元素包含这样的长度
function sumAsm(uint[] _data) returns (uint o_sum) {
for (uint i = 0; i < _data.length; ++i) {
assembly {
o_sum := add(o_sum, mload(add(add(_data, 0x20), mul(i, 0x20))))
}
}
}
}
语法
汇编跟solidity一样,会解析注释,字面量和标识符。所以你可以使用//
和/* */
来写注释。内联编译用assembly {}
标记,代码在花括号中。会使用下面的规则(在以后的章节中会详细描述):
- 字面量,例如
0x123
,42
或者abc
(字符串最多32个字符) - 操作符(指令类型),例如
mload sload dup1 sstore
,更多的操作符号,查看下面的列表。 - 函数类型的操作符,例如
add(1, mlod(0))
- 标签,如
name
- 变量声明,例如
let x := 7
,let x := add(y, 3)
或者let x
(初始值为empty(0)) - 标识符(标签或者汇编的变量和内联汇编情况下的外部变量),例如
jump(name)
,3 x add
- 赋值(指令形式),例如
3 =: x
- 函数形式的赋值,例如
x := add(y, 3)
- 包含局部变量的区块,例如
{ let x := 3 { let y := add(x, 1) } }
操作码
本文档不会完全描述以太坊虚拟机,但是下面的列表是操作码集合,这有必要了解。
如果操作码包含参数(总是从栈顶取数据),会有圆括号。注意非函数类型的预留参数的顺序(在下面阐述)。操作码有-
标记的是不需要将数据推入堆栈的,有*
标记的会有些特别,其他的都会往堆栈中推入一个数据。
在下面的列表中,mem[a ... b)
表明从a
开始到b
但是不包括b
分配这么多字节的内存。storage[p]
表明p
位置的数据。
操作码push1
和jumpdest
不能直接使用。
在语法中,操作码相当于预定义的标识符。
操作码 | 标记 | 解释 |
---|---|---|
stop | - | 停止执行,相当于返回(0,0) |
add(x,y) | x + y | |
sub(x,y) | x - y | |
mul(x,y) | x * y | |
div(x,y) | x / y | |
sdiv(x,y) | x / y ,对于有符号数,用补码 | |
mod(x,y) | x % y | |
smod(x,y) | x % y,对于有符号数,用补码 | |
exp(x,y) | x的y次方 | |
not(x) | ~x,按位取反 | |
lt(x,y) | 如果x < y,返回1,否则返回0 | |
gt(x,y) | 如果x > y,返回1,否则返回0 | |
slt(x,y) | 如果x < y,返回1,否则返回0,对于有符号数,用补码 | |
sgt(x,y) | 如果x > y,返回1,否则返回0,对于有符号数,用补码 | |
eq(x,y) | 如果x == y,返回1,否则返回0 | |
iszero(x) | 如果x == 0,返回1,否则返回0 | |
and(x,y) | x和y按位与 | |
or(x,y) | x和y按位或 | |
xor(x,y) | x和y按位异或 | |
byte(n,x) | x的第n位,最重要的是第0位 | |
addmod(x,y,m) | (x + y) % m,可以是任意精度的算术 | |
mulmod(x,y,m) | (x * y) % m,可以是任意精度的算术 | |
signxtend(i,x) | 从第(i * 8 + 7)位开始数最少签名位数,来扩展签名 | |
keccak256(p,n) | keccak(mem[p...(p+n))) | |
sha3(p,n) | sha3(mem[p...(p+n))) | |
jump(label) | - | 跳转到label标签/代码位置 |
jumpi(label, cond) | - | 如果cond非零,就跳转到label标签 |
pc | 代码的当前位置 | |
pop(x) | - | 删除x推入的元素 |
dup1 ... dup16 | 拷贝第i位的元素到栈顶(从顶端开始数) | |
swap1 ... swap16 | * | 交换栈顶和第i位的元素 |
mload(p) | mem[p..(p+32)) | |
mstore(p, v) | - | mem[p..(p+32)) := v |
mstore8(p, v) | - | mem[p] := v & 0xff - 只分配一个字节 |
sload(p) | storage[p] | |
sstore(p, v) | - | storage[p] := v |
msize | 内存的大小,例如最大的内存可访问索引 | |
gas | 可用的gas数 | |
address | 当前合约/执行上下文的地址 | |
balance(a) | 地址a的余额,单位为wei | |
caller | 调用者(不包含delegatecall) | |
callvalue | 当前调用发送的wei数 | |
calldataload(p) | 从位置p开始的调用数据(32位) | |
calldatasize | 调用数据的大小,bytes为单位 | |
calldatacopy(t,f,s) | - | 从数据位置f,拷贝s位数据,到内存位置t |
codesize | 当前合约/执行上下文的代码大小 | |
codecopy(t,f,s) | - | 从代码位置f,拷贝s位数据,到内存位置t |
extcodesize(a) | 地址a处代码的大小 | |
extcodecopy(a, t, f, s) | - | 像codecopy(t,f,s),但是是位置a处的代码 |
returndatasize | 上个返回值的数据大小 | |
returndatacopy(t, f, s) | - | 在返回数据的位置f拷贝s位,到内存位置t |
create(v, p, s) | 用 mem[p..(p+s))长度的代码,创建一个新的合约,发送v wei的以太币,并返回合约地址 | |
create2(v, n, p, s) | 用 地址为keccak256(< address > . n . keccak256(mem[p..(p+s))),mem[p..(p+s))长度的代码,创建一个新的合约,发送v wei的以太币,并返回合约地址 | |
call(g, a, v, in, insize, out, outsize) | 调用地址a上的合约,参数为mem[in..(in+insize)),提供g的gas,v wei的以太币,输出到mem[out..(out+outsize)),如果成功返回1,失败(例如gas不足)返回0 | |
callcode(g, a, v, in, insize, out, outsize) | 与call相同,但是使用a处的代码,并只能在当前合约的上下文中执行 | |
delegatecall(g, a, in, insize, out, outsize) | 与callcode相同,但是保留caller 和callvalue |
|
staticcall(g, a, in, insize, out, outsize) | 与_call(g, a, 0, in, insize, out, outsize) _相同,但是不允许改变状态 | |
return(p, s) | - | 结束执行,返回数据mem[p..(p+s)) |
revert(p, s) | - | 结束执行,恢复状态变化,返回数据mem[p..(p+s)) |
selfdestruct(a) | - | 结束执行,销毁当前合约,并把余额发送给a地址 |
invalid | - | 用invalid指令结束执行 |
log0(p,s) | - | 记录日志,不包含主题,数据为mem[p..(p+s) |
log1(p,s,t1) | - | 记录日志,包含主题t1,数据为mem[p..(p+s) |
log2(p,s,t1,t2) | - | 记录日志,包含主题t1,t2,数据为mem[p..(p+s) |
log3(p,s,t1,t2,t3) | - | 记录日志,包含主题t1,t2,t3,数据为mem[p..(p+s) |
log4(p,s,t1,t2,t3,t4) | - | 记录日志,包含主题t1,t2,t3,t4,数据为mem[p..(p+s) |
origin | 交易发起方 | |
gasprice | 交易的gas价格 | |
blockhash(b) | 区块序列为b的hash - 只能获取到当前块的最近256块 | |
coinbase | 当前的矿工收益 | |
timestamp | 自创世纪区块以来的时间戳,单位为秒 | |
number | 当前区块的序号 | |
difficulty | 当前区块的难度 | |
gaslimit | 当前代码块的gas限制 |
字面量
你可以使用整形常量,可以用十进制或十六进制的写法,一个合适的PUSHi
指令会自动的生成。下面的代码执行2加3等于5,然后计算位宽,然后与字符串"abc"相加。字符串是左对齐保存的,并且不能超过32位。
assembly { 2 3 add "abc" and }
函数类型
你可以用同样的方式在操作码之后输入操作码,它们会以字节码结束。例如,给内存0x80
处的值加3的代码是:
3 0x80 mload add 0x80 mstore
由于这种形式很难看出来操作码的实际参数,Solidity内联编译也提供“函数形式”的写法,如下代码与上面的代码功能相同:
mstore(0x80, add(mload(0x80), 3))
函数形式的表达式内部不能使用指令类型。例如,1 2 mstore(0x80, add)
是不允许的。必须要写成mstore(0x80, add(2, 1))
。如果操作码没有参数,那么圆括号就可以省略。
注意,参数顺序是和函数类型的参数顺序相反,如果你使用函数类型,第一个参数应该在栈顶。
访问外部变量和函数
Solidity的变量和其他标识符可以通过使用名称来简单访问。对金钱变量,将会把地址,而不是金额推入堆栈。storage变量有些不同:storage的值可能不会占据一个完整的storage片,所以它们的地址是由一个片地址和位偏移组成。为了得到变量x
的片地址,使用x_slot
,获取偏移使用x_offset
。
在赋值操作中(如下所示),我们可以使用Solidity变量去赋值。
内联编译外部的函数,也是可以访问的:汇编会把它们的入口标签(使用虚拟函数解决方法)压入堆栈。Solidity中的调用语法如下:
- 调用者压入标签,arg1,arg2,...,argn
- 被调用返回ret1,ret2,...,retm
这个功能使用起来还是比较笨拙,因为栈偏移会在调用中变化,所以引用的值也会出错。
pragma solidity ^0.4.11;
contract C {
uint b;
function f(uint x) returns (uint r) {
assembly {
r := mul(x, sload(b_slot)) // 这里忽略了偏移,因为我们知道,偏移为0
}
}
}
标签(labels)
EVM汇编的另一个问题是jump
和jumpi
使用的是绝对地址,可能会很容易变动。Solidity内联编译提供标签来使跳转更加容易。注意,标签是底层特性,不用标签,只用汇编函数,循环和switcht指令的效率可能会更高(看下面的例子)。下面的代码是计算斐波那契数列。
{
let n := calldataload(4)
let a := 1
let b := a
loop:
jumpi(loopend, eq(n, 0))
a add swap1
n := sub(n, 1)
jump(loop)
loopend:
mstore(0, a)
return(0, 0x20)
}
请注意,自动访问栈变量只能在编译器知道当前栈的深度的情况有效。如果跳转的深度和目标的深度不同,就会失败。但是使用jumps还是可以的,但是这种情况下你不能访问栈变量了(即使是汇编变量)。
另外,栈深度分析器对opcode逐一分析(不是根据控制流),所以在下面的例子中,编译器会对two
标签的深度有错误的分析。
{
let x := 8
jump(two)
one:
// 这里栈深度是2(因为我们压入了x和7)
// 但是汇编器认为深度只有1,因为它是自顶向下读取的。
// 这里访问变量x会导致错误。
x := 9
jump(three)
two:
7 // 在堆栈中压入数据
jump(one)
three:
}
这个问题可以手动的调整栈深度来修复-你可以在标签之前提供一个栈深度偏移。注意,你不必关心这这些事情,如果你使用循环和汇编局部函数。
极端情况下的例子如下所示:
{
let x := 8
jump(two)
0 // 这个代码是可达的,但是会修正栈深度
one:
x := 9 // x是可访问的
jump(three)
pop // 类似于负校正
two:
7 // 压入数据
jump(one)
three:
pop // 我们必须要手动的弹出压入的数据
}
声明汇编局部变量(Declaring Assembly-Local Variables)
我们可以使用let
关键字来声明变量,但是这些变量只能在内联编译代码块中可见。也就是只能在{...}
块中。这里的原理是,let
指令会生成一个新的栈slot,保留给变量,并且当代码块结束的时候自动的移除。你需要为变量提供初始值,可以是0
,但是也可以是复杂的函数表达式。
pragma solidity ^0.4.0;
contract C {
function f(uint x) returns (uint b) {
assembly {
let v := add(x, 1)
mstore(0x80, v)
{
let y := add(sload(v), 1)
b := y
} // y会被回收
b := add(b, v)
} // v被回收
}
}
赋值
汇编局部变量和函数局部变量的赋值是可以实现的。注意对指向内存或storage的变量赋值时,你改变的是指针,而不是数据。
有两种类型的赋值:函数形式的和指令形式的。对于函数形式的赋值(variable := value
),你要在函数类型的表达式中提供值,并会返回一个栈值。对于指令形式的(=: variable
),值从栈顶取。对于这两种形式,冒号指向的是变量。赋值的操作是用新的值替换栈中的变量值。
{
let v := 0 // 声明变量,函数形式的赋值
let g := add(v, 2)
sload(10)
=: v // 指令形式的赋值,把 sload(10)的结果赋值给v
}
switch
你可以像很基本的“if/else“一样,来使用switch表达式。它会拿着表达式的值和多个条件对比。对应的程序分支会被执行。和一些容易出错的语言相比,这里的控制流不会接着执行下一个分支。switch可以有一个默认分支,称为default
:
{
let x := 0
switch calldataload(4)
case 0 {
x := calldataload(0x24)
}
default {
x := calldataload(0x44)
}
sstore(0, div(x, 2))
}
分支不需要包裹大括号,但是分支需要大括号。
循环
汇编支持简单的for形式的循环。for形式的循环,有一个头部,包含初始条件,条件和遍历结束条件。条件必须是函数形式的表达式。但是其他两个是代码块。如果初始部分声明了任何变量,这些变量的作用域可以延伸到函数体(包含条件和遍历结束条件的部分)。
下面的例子会计算一个内存区域的和:
{
let x := 0
for { let i := 0 } lt(i, 0x100) { i := add(i, 0x20) } {
x := add(x, mload(i))
}
}
函数
汇编支持定义更底层的函数。函数从堆栈中获取参数(和一个程序计数器PC),并且把结果返回到堆栈中。调用函数看起来只是执行函数形式的操作码。
函数可以定义在任何地方,并且在整个代码块中可见。在函数中,你不能访问在函数外部定义的局部变量。函数也没有明确的return
表达式。
如果你调用一个函数,并返回多个值,你可以把它们赋值给元祖。a,b := f(x)
或者let a,b := f(x)
。
下面的例子通过平方和乘法,实现了求幂功能。
{
function power(base, exponent) -> result {
switch exponent
case 0 { result := 1 }
case 1 { result := base }
default {
result := power(mul(base, base), div(exponent, 2))
switch mod(exponent, 2)
case 1 { result := mul(base, result) }
}
}
}
要避免的要点
内联编译可能看起来像是高层次的,但是它确实是底层接口。函数调用,循环和switch分支会被转变为简单的重写规则,并且在那之后,编译器为你做的事情只是重新整理函数形式的操作码,管理跳转标签,计算可访问的栈深度,在代码块结束的时候移除汇编局部变量的栈内存。尤其是最后两个情况,必须要清楚,编译器只会自顶向下的计算栈深度,而不是跟随控制流。另外交换只会交换栈里的内容,而不会交换变量的指向。
Solidity约定
和EVM的汇编相比,Solidity可以知道比256位更窄的类型,例如uint24
。为了提高效率,很多算术操作符会把它们看成是256位的,并且高位只会在有需要的时候清除。例如,在写入内存的时候要缩短,或者在比较的时候。这意味这,如果你在内联编译中访问这些变量,那你要首先手动的去除高位。
Solidity用一种很简单的方式来管理内存:在0x40
有一个空白内存指针。如果你要分配内存,只需要使用该指针指向的内存,并相应的更新指针。
Solidity中的内存数组元素,只会占据32位的倍数(对的,对于byte[]
也是一样的,但是bytes
和string
就不一样了)。多维内存数组会指向内存数组。动态数组的长度会保存在数组的第一个slot中,然后后面的都是数组的值。
警告:静态大小的内存数组不会有长度的字段,但是会在以后的版本中加上,以提高静态数组--动态数组的可转换性,所以现在不要做这样的转换。
独立汇编(Standalone Assembly)
如上所说的汇编语言的内联编译,也可以用独立编译。事实上,计划用它作为solidity的中间语言。在这种情况下,它有如下几个目的:
- 用它写的代码可读,即使是solidity编译器生成的代码,也是可读的。
- 从汇编到字节码的转换的黑魔法应该越少越好。
- 控制流可以方便的监测来有助格式检测和优化。
为了达到第一个和最后一个目标,汇编提供了高级的指令,像for
循环,switch
表达式和函数调用。而不需要用到SWAP
,DUP
,JUMP
和JUMPI
,因为前两个会扰乱数据流,后两个会扰乱控制流。另外,mul(add(x,y), 7)
形式的函数表达式,好过操作码形的7 y x add mul
,因为第一种形式更加直观的看出操作码用的操作数。
第二个目标的实现,通过引入去语法糖的短语--只是移除了高阶构指令--并允许检测生成的低阶的汇编码。编译器唯一的非局部操作是为用户定义的标识符(函数,变量...)查找名称,该过程遵循简单的作用域规则和清理堆栈中的局部变量。
作用域:标识符(标签,变量,函数,汇编(?assembly))只能在被声明的代码块中可见(包含当前块的嵌入代码块)。不允许跨代码块访问局部变量,即使它们在作用域中。影子调用(?shadowing)是不允许的。局部变量必须先定义后使用。但是标签,函数和汇编可以。汇编是特殊的代码块,例如,用来返回运行时操作码或者创建合约。外部的标识符是不能在内部访问。
如果控制流执行到代码块结尾,会插入于本地变量数目相同的pop指令。局部变量无论在什么时候被引用,代码生成器必须知道堆栈当前的相对位置,并且要跟踪栈的深度。由于所有的局部变量会在代码结束的时候被移除,代码执行之前和执行之后的栈深度是一致的。如果不是这种情况,会出现一个警告。
我们需要高阶指令--例如switch
,for
和函数--的原因是:
使用switch
,for
和函数,可以不用jump
和jumpi
来实现复杂的代码。分析控制流也变得更加容易了,这会提高格式校验和优化的效率。
另外,如果允许手动跳转,计算栈的深度也是比较复杂的。所有局部变量的位置必须知道,另外还要正确引用变量,在代码块结束的时候正确的清理变量。如果不是连续的执行流,去语法糖机制能够在不可达的地方,正确的插入操作来较正栈深度。
例子:
我们分析一个例子从Solidity到去语法糖的汇编来说明原理。我们会分析下面Solidity代码的字节码:
pragma solidity ^0.4.0;
contract C {
function f(uint x) returns (uint y) {
y = 1;
for (uint i = 0; i < x; i++)
y = 2 * y;
}
}
下面的是生成的汇编:
{
mstore(0x40, 0x60) // 保存空白内存指针
// 函数调度
switch div(calldataload(0), exp(2, 226))
case 0xb3de648b {
let (r) = f(calldataload(4))
let ret := $allocate(0x20)
mstore(ret, r)
return(ret, 0x20)
}
default { revert(0, 0) }
// 内存分配
function $allocate(size) -> pos {
pos := mload(0x40)
mstore(0x40, add(pos, size))
}
// 合约地址
function f(x) -> y {
y := 1
for { let i := 0 } lt(i, x) { i := add(i, 1) } {
y := mul(2, y)
}
}
}
在去语法糖处理之后,代码如下:
{
mstore(0x40, 0x60)
{
let $0 := div(calldataload(0), exp(2, 226))
jumpi($case1, eq($0, 0xb3de648b))
jump($caseDefault)
$case1:
{
// 函数调用- 我们把返回标签和参数压入堆栈
$ret1 calldataload(4) jump(f)
// 这是不可达代码,添加的操作码用来调节栈深度:参数会移除,引入返回值
pop pop
let r := 0
$ret1: // 真正的返回点
$ret2 0x20 jump($allocate)
pop pop let ret := 0
$ret2:
mstore(ret, r)
return(ret, 0x20)
// 尽管这是没有用的,这个跳转是自动插入的。因为去语法糖处理是存粹的语法操作,
// 不会分析控制流
jump($endswitch)
}
$caseDefault:
{
revert(0, 0)
jump($endswitch)
}
$endswitch:
}
jump($afterFunction)
allocate:
{
// 我们跳过不可达代码
jump($start)
let $retpos := 0 let size := 0
$start:
// 输出变量和参数有一样的作用域,并且被分配
let pos := 0
{
pos := mload(0x40)
mstore(0x40, add(pos, size))
}
// 这个代码用返回值和跳转来替换参数
swap1 pop swap1 jump
// 这里也是不可达代码,用来矫正栈的深度
0 0
}
f:
{
jump($start)
let $retpos := 0 let x := 0
$start:
let y := 0
{
let i := 0
$for_begin:
jumpi($for_end, iszero(lt(i, x)))
{
y := mul(2, y)
}
$for_continue:
{ i := add(i, 1) }
jump($for_begin)
$for_end:
} // 这里会为i插入一个pop指令
swap1 pop swap1 jump
0 0
}
$afterFunction:
stop
}
汇编会在四个阶段发生:
- 解析
- 去语法糖(移除 switch,for和函数)
- 操作码流生成
- 字节码生成
我们会用伪代码的方式来分析第一步到第三步。下面是正规的阐述。(?More formal specifications will follow.)
解析/语法
解析器的任务如下:
- 把字节码流转换为令牌流,移除c++格式的注释(一种用于代码引用的特殊注释,但是我们在这里不会再解释)(?Turn the byte stream into a token stream, discarding C++-style comments (a special comment exists for source references, but we will not explain it here).)
- 根据如下的语法,将令牌流转换为AST。
- 在标识符定义的代码块中注册标识符(注释为AST节点)并且注明指向的节点,以及可以访问的变量。
汇编词法分析器会遵循Solidity自己定义的规则,
空白字符用来分离令牌,它包含空格键,Tab键和换行。注释按照传统的javascript/c++的方式,它们会被替换为空白字符。
语法:
AssemblyBlock = '{' AssemblyItem* '}'
AssemblyItem =
Identifier |
AssemblyBlock |
FunctionalAssemblyExpression |
AssemblyLocalDefinition |
FunctionalAssemblyAssignment |
AssemblyAssignment |
LabelDefinition |
AssemblySwitch |
AssemblyFunctionDefinition |
AssemblyFor |
'break' | 'continue' |
SubAssembly | 'dataSize' '(' Identifier ')' |
LinkerSymbol |
'errorLabel' | 'bytecodeSize' |
NumberLiteral | StringLiteral | HexLiteral
Identifier = [a-zA-Z_$] [a-zA-Z_0-9]*
FunctionalAssemblyExpression = Identifier '(' ( AssemblyItem ( ',' AssemblyItem )* )? ')'
AssemblyLocalDefinition = 'let' IdentifierOrList ':=' FunctionalAssemblyExpression
FunctionalAssemblyAssignment = IdentifierOrList ':=' FunctionalAssemblyExpression
IdentifierOrList = Identifier | '(' IdentifierList ')'
IdentifierList = Identifier ( ',' Identifier)*
AssemblyAssignment = '=:' Identifier
LabelDefinition = Identifier ':'
AssemblySwitch = 'switch' FunctionalAssemblyExpression AssemblyCase*
( 'default' AssemblyBlock )?
AssemblyCase = 'case' FunctionalAssemblyExpression AssemblyBlock
AssemblyFunctionDefinition = 'function' Identifier '(' IdentifierList? ')'
( '->' '(' IdentifierList ')' )? AssemblyBlock
AssemblyFor = 'for' ( AssemblyBlock | FunctionalAssemblyExpression)
FunctionalAssemblyExpression ( AssemblyBlock | FunctionalAssemblyExpression) AssemblyBlock
SubAssembly = 'assembly' Identifier AssemblyBlock
LinkerSymbol = 'linkerSymbol' '(' StringLiteral ')'
NumberLiteral = HexNumber | DecimalNumber
HexLiteral = 'hex' ('"' ([0-9a-fA-F]{2})* '"' | '\'' ([0-9a-fA-F]{2})* '\'')
StringLiteral = '"' ([^"\r\n\\] | '\\' .)* '"'
HexNumber = '0x' [0-9a-fA-F]+
DecimalNumber = [0-9]+
去语法糖(desugaring)
AST变换会移除for,switch和函数指令。结果还是可以通过相同的分析器分析的。但是不会用到特定的指令。如果使用了跳转,不是连续执行,那么就要加入关于堆栈的信息。除非没有局部变量或者外部作用域的变量被访问到,或者指令执行之前和指令执行之后的堆栈深度是相同的。
伪代码:
desugar item: AST -> AST =
match item {
AssemblyFunctionDefinition('function' name '(' arg1, ..., argn ')' '->' ( '(' ret1, ..., retm ')' body) ->
:
{
jump($_start)
let $retPC := 0 let argn := 0 ... let arg1 := 0
$_start:
let ret1 := 0 ... let retm := 0
{ desugar(body) }
swap and pop items so that only ret1, ... retm, $retPC are left on the stack
jump
0 (1 + n times) to compensate removal of arg1, ..., argn and $retPC
}
AssemblyFor('for' { init } condition post body) ->
{
init // 不能在它自己的代码块里,因为我们期望变量的作用域扩展到整个代码体
// 找到 I, 没有 $forI_* 标签。
$forI_begin:
jumpi($forI_end, iszero(condition))
{ body }
$forI_continue:
{ post }
jump($forI_begin)
$forI_end:
}
'break' ->
{
// 找到标签$forI_end 最近的作用域
// 弹出在这个点上,定义的所有变量,但是不是在 $forI_end 上
jump($forI_end)
0 (很多上面的变量被移除了)
}
'continue' ->
{
// 找到标签$forI_continue 最近的作用域
// 弹出在这个点上,定义的所有变量,但是不是在 $forI_continue
jump($forI_continue)
0 (很多上面的变量被移除了)
}
AssemblySwitch(switch condition cases ( default: defaultBlock )? ) ->
{
// 找到I,直到没有$switchI* 标签或者变量
let $switchI_value := condition
for each of cases match {
case val: -> jumpi($switchI_caseJ, eq($switchI_value, val))
}
if default block present: ->
{ defaultBlock jump($switchI_end) }
for each of cases match {
case val: { body } -> $switchI_caseJ: { body jump($switchI_end) }
}
$switchI_end:
}
FunctionalAssemblyExpression( identifier(arg1, arg2, ..., argn) ) ->
{
if identifier is function with n args and m ret values ->
{
// 找到 I 直到 $funcallI_* 标签不存在
$funcallI_return argn ... arg2 arg1 jump()
pop (n + 1 times)
if the current context is `let (id1, ..., idm) := f(...)` ->
let id1 := 0 ... let idm := 0
$funcallI_return:
else ->
0 (m times)
$funcallI_return:
turn the functional expression that leads to the function call
into a statement stream
}
else -> desugar(children of node)
}
default node ->
desugar(children of node)
}
操作码流生成(Opcode Stream Generation)
在操作码流生成的过程中,我们用计数器来跟踪堆栈的深度,来使得堆栈变量可以访问。堆栈深度能够被每个能够改变堆栈的操作码和用来注释堆栈矫正的标签所改变。每当新的变量被声明,堆栈深度也会被改变。如果访问一个变量(要么拷贝它的值,要么赋值),会依据当前堆栈的深度和变量被引入点的深度来选择使用合适的DUP或者SWAP指令。
伪代码:
codegen item: AST -> opcode_stream =
match item {
AssemblyBlock({ items }) ->
join(codegen(item) for item in items)
if last generated opcode has continuing control flow:
POP for all local variables registered at the block (including variables
introduced by labels)
warn if the stack height at this point is not the same as at the start of the block
Identifier(id) ->
lookup id in the syntactic stack of blocks
match type of id
Local Variable ->
DUPi where i = 1 + stack_height - stack_height_of_identifier(id)
Label ->
// 在生成字节码过程中,引用会被解决
PUSH
SubAssembly ->
PUSH
FunctionalAssemblyExpression(id ( arguments ) ) ->
join(codegen(arg) for arg in arguments.reversed())
id (which has to be an opcode, might be a function name later)
AssemblyLocalDefinition(let (id1, ..., idn) := expr) ->
register identifiers id1, ..., idn as locals in current block at current stack height
codegen(expr) - assert that expr returns n items to the stack
FunctionalAssemblyAssignment((id1, ..., idn) := expr) ->
lookup id1, ..., idn in the syntactic stack of blocks, assert that they are variables
codegen(expr)
for j = n, ..., i:
SWAPi where i = 1 + stack_height - stack_height_of_identifier(idj)
POP
AssemblyAssignment(=: id) ->
look up id in the syntactic stack of blocks, assert that it is a variable
SWAPi where i = 1 + stack_height - stack_height_of_identifier(id)
POP
LabelDefinition(name:) ->
JUMPDEST
NumberLiteral(num) ->
PUSH
HexLiteral(lit) ->
PUSH32
StringLiteral(lit) ->
PUSH32
SubAssembly(assembly block) ->
append codegen(block) at the end of the code
dataSize() ->
assert that is a subassembly ->
PUSH32>
linkerSymbol() ->
PUSH32 and append position to linker table
}