Yul
Yul(先前被也被称为 JULIA 或 IULIA)是一种可以编译到各种不同后端的中间语言。
计划支持EVM 1.0,EVM 1.5和Ewasm,它被设计为这三个平台的可用的共同标准。 它已经可以在独立模式下使用,也可以在Solidity内部用于 “内联汇编”, 并且有一个Solidity编译器的实验性实现,将Yul作为一种中间语言。 Yul是高级优化阶段的一个很好的目标,可以使所有的目标平台同样受益。
动机和高级别描述
Yul 的设计试图实现几个目标:
用Yul编写的程序应该是可读的,即使代码是由Solidity或其他高级语言的编译器生成的。
控制流应易于理解,以帮助人工检查、形式化验证和优化。
从Yul到字节码的翻译应该尽可能的简单明了。
Yul应该适用于整个程序的优化。
为了实现第一个和第二个目标,Yul提供了高级别的结构,如 for
循环, if
和 switch
语句和函数调用。
这些应该足以充分代表汇编程序的控制流。
因此,没有提供 SWAP
, DUP
, JUMPDEST
, JUMP
和 JUMPI
的明确语句,
因为前两者混淆了数据流,后两者混淆了控制流。此外,
mul(add(x, y), 7)
形式的函数语句比 7 y x add mul
这样的纯操作码语句更受欢迎,
因为在第一种形式中,更容易看到哪个操作数用于哪个操作码。
尽管它是为堆栈机设计的,但Yul并没有暴露堆栈本身的复杂性。 程序员或审计师不应该担心堆栈的问题。
第三个目标是通过以一种非常有规律的方式将高层结构编译成字节码来实现的。 汇编器执行的唯一非本地操作是用户定义的标识符(函数、变量......)的名称查找和清理堆栈中的本地变量。
为了避免值和引用等概念之间的混淆, Yul是静态类型的。同时, 有一个默认的类型(通常是目标机的整数字), 可以随时省略以帮助增加可读性。
为了保持语言的简单和灵活, Yul在其纯粹的形式下没有任何内置的操作,函数或类型。 在指定Yul的语言时,这些操作和语义被添加到一起, 这使得Yul可以根据不同的目标平台和功能集的要求进行专业化。
目前,只有一种指定的Yul语言。这个语言使用EVM的操作码作为内建函数(见下文),
并且只定义了 u256
类型,这是EVM的本地256位类型。正因为如此,我们将不在下面的例子中提供类型。
简单的例子
下面的例子程序是用EVM语言编写的,用来计算指数。
它可以用 solc --strict-assembly
指令编译。
内置函数 mul
和 div
分别计算乘法和除法。
{
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) }
}
}
}
也可以用for-loop而不是递归来实现同样的函数。
这里, lt(a, b)
计算 a
是否小于 b
。
{
function power(base, exponent) -> result
{
result := 1
for { let i := 0 } lt(i, exponent) { i := add(i, 1) }
{
result := mul(result, base)
}
}
}
在 本节的末尾 ,可以找到ERC-20标准的完整实现。
单独使用
您可以使用Solidity编译器在EVM语言中以独立的形式使用Yul。
这将使用 Yul 对象符号,这样就有可能将代码作为数据引用到部署合约中。
这种Yul模式可用于命令行编译器(使用 --strict-assembly
)和 标准-json接口。
{
"language": "Yul",
"sources": { "input.yul": { "content": "{ sstore(0, 1) }" } },
"settings": {
"outputSelection": { "*": { "*": ["*"], "": [ "*" ] } },
"optimizer": { "enabled": true, "details": { "yul": true } }
}
}
警告
Yul正在积极开发中,只有以EVM 1.0为目标,Yul的EVM语言才能完全实现字节码生成。
对Yul的非正式描述
在下文中,我们将谈论Yul语言的每个单独方面。在例子中,我们将使用默认的EVM语言。
语法
Yul使用与Solidity相同的方式解析注释,字词和标识符,
所以您可以使用 //
和 /* */
来表示注释。
但是有一个例外,Yul中的标识符可以包含圆点: .
。
Yul可以指定由代码,数据和子对象组成的 “对象”。 请参阅 Yul 对象 以了解这方面的详情。 在本节中,我们只关注这样一个对象的代码部分。 这个代码部分总是由一个大括号限定的块组成。 大多数工具都支持只指定一个预期对象的代码块。
在一个代码块内,可以使用以下元素(更多细节见后面章节):
字母,即
0x123
,42
或"abc"
(32个字符以内的字符串)。对内置函数的调用,例如
add(1, mload(0))
变量声明,例如
let x := 7
,let x := add(y, 3)
或let x
(初始值为0)标识符(变量),例如:
add(3, x)
赋值,例如:
x := add(y, 3)
局部变量的作用域所在的代码块,例如
{ let x := 3 { let y := add(x, 1) } }
if 语句,例如
if lt(a, b) { sstore(0, 1) }
switch语句,例如
switch mload(0) case 0 { revert() } default { mstore(0, 1) }
for 循环,例如
for { let i := 0} lt(i, 10) { i := add(i, 1) } { mstore(i, 7) }
函数的定义,例如
function f(a, b) -> c { c := add(a, b) }
多个语法元素之间可以简单地用空格隔开,即不需要结尾的 ;
或换行。
字面量
作为字面量,您可以使用。
以十进制或十六进制符号表示的整数常数。
ASCII字符串(例如
"abc"
),可能包含十六进制转义\xNN
和 Unicode转义\uNNNN
,其中N
是十六进制数字。十六进制字符串(例如:
hex"616263"
)。
在Yul的EVM语言中,字面量表示256位的单词,如下所示:
十进制或十六进制的常量必须小于
2**256
。 它们以大端编码的无符号整数形式表示具有该值的256位字。一个ASCII字符串首先被看作是一个字节序列, 通过将非转义ASCII字符看作是一个单字节,其值是ASCII代码, 转义
\xNN
是具有该值的单字节, 转义\uNNNN
是该代码点的UTF-8字节序列。 字节序列不得超过32字节。 字节序列在右边用零填充,以达到32个字节的长度; 换句话说,字符串是以左对齐的方式存储。 填充后的字节序列代表一个256位的字,其最有意义的8位是第一个字节的1, 也就是说,字节被解释为大端形式。十六进制字符串首先被视为一个字节序列, 将每一对连续的十六进制数字视为一个字节。 字节序列不得超过32个字节(即64个十六进制数字),并按上述方法处理。
当为EVM编译时,这将被翻译成一个适当的 PUSHi
指令。
在下面的例子中, 3
和 2
相加的结果是 5,
然后计算与字符串 “abc” 的按位 与(and)
。
最后的数值被分配到一个叫做 x
的局部变量。
上述32字节的限制并不适用于传递给需要字面参数的内置函数的字符串(例如, setimmutable
或 loadimmutable
)。
这些字符串最终不会出现在生成的字节码中。
let x := and("abc", add(3, 2))
除非是默认类型,否则字面的类型必须在冒号后指定:
// 这将不会被编译(u32和u256类型尚未实现)。
let x := and("abc":u32, add(3:u256, 2:u256))
函数调用
内置函数和用户定义的函数(见下文)都可以用前面例子中的相同方式调用。 如果函数返回一个单一的值,它可以直接在一个表达式中再次使用。 如果它返回多个值,则必须将它们分配给局部变量。
function f(x, y) -> a, b { /* ... */ }
mstore(0x80, add(mload(0x80), 3))
// 此处,用户定义的函数 `f` 返回两个值。
let x, y := f(1, mload(0))
对于EVM的内置函数,函数表达式可以直接转换为操作码流:
您只需从右到左读取表达式,就可以得到操作码。
在例子中的第一行,是 PUSH1 3 PUSH1 0x80 MLOAD ADD PUSH1 0x80 MSTORE
。
对于调用用户定义的函数,参数也从右到左放在堆栈中,这是参数列表被评估的顺序。
然而,返回值是在堆栈中从左到右,即在这个例子中, y
在堆栈的顶部, x
在其下方。
变量声明
您可以使用 let
关键字来声明变量。变量只在它所定义的 {...}
块内可见。
当编译到EVM时,会创建一个新的堆栈槽,为该变量保留,并在到达块的末端时自动移除。
您可以为该变量提供一个初始值。如果您不提供一个值,该变量将被初始化为零。
由于变量存储在堆栈中,它们不直接影响内存或存储,
但它们可以在内置函数 mstore
, mload
, sstore
和 sload
中作为内存或存储位置的指针使用。
未来的语言可能会为这种指针引入特定的类型。
当一个变量被引用时,其当前值被复制。
对于EVM来说,这相当于一个 DUP
指令。
{
let zero := 0
let v := calldataload(zero)
{
let y := add(sload(v), 1)
v := y
} // y在这里被 “删除” 了
sstore(v, zero)
} // v和zero在这里被 “删除”。
如果声明的变量应该有一个与默认类型不同的类型,您可以用冒号表示。 当您从一个返回多个值的函数调用中赋值时,您也可以在一条语句中声明多个变量。
// 这将不会被编译(u32和u256类型尚未实现)。
{
let zero:u32 := 0:u32
let v:u256, t:u32 := f()
let x, y := g()
}
根据优化器的设置,编译器可以在变量被最后一次使用后释放堆栈槽,即使它仍然在范围内。
赋值
变量可以在其定义后使用 :=
操作符进行赋值。可以在同一时间对多个变量进行赋值。
为此,数值的数量和类型必须匹配。如果您想对一个有多个返回参数的函数进行赋值,
您必须提供多个变量。同一变量不能多次出现在赋值的左侧,例如: x, x := f()
是无效的。
let v := 0
// 重新对v赋值
v := 2
let t := add(v, 2)
function f() -> a, b { }
// 赋予多个值
v, t := f()
If
if语句可用于有条件地执行代码。不能定义 “else” 块。 如果您需要多种选择条件,可以考虑使用 “switch” 来代替(见下文)。
if lt(calldatasize(), 4) { revert(0, 0) }
代码块的大括号是必需的。
Switch
您可以使用switch语句作为if语句的扩展版本。
它获取一个表达式的值,并将其与几个字面常量进行比较,与匹配的常量相对应的分支被选中。
与其他编程语言不同的是,出于安全考虑,控制流不会从一个条件延续到下一个条件。
可以有一个叫 default
的回退或默认情况,如果没有一个字面常数匹配,就会采取这种情况。
{
let x := 0
switch calldataload(4)
case 0 {
x := calldataload(0x24)
}
default {
x := calldataload(0x44)
}
sstore(0, div(x, 2))
}
条件的列表没有用大括号括起来,但条件的主体确实需要大括号。
循环
Yul支持for循环,它由一个包含初始化部分的头,一个条件,一个后迭代部分和一个主体组成。 条件必须是一个表达式,而其他三个是代码块。 如果初始化部分在顶层声明了任何变量,这些变量的范围将延伸到循环的所有其他部分。
break
和 continue
语句可以在主体中使用,分别用于退出循环或跳到后部分。
下面的例子是计算内存中一个代码区域的总和。
{
let x := 0
for { let i := 0 } lt(i, 0x100) { i := add(i, 0x20) } {
x := add(x, mload(i))
}
}
For循环也可以作为while循环的替代: 只需将初始化和后迭代部分留为空即可。
{
let x := 0
let i := 0
for { } lt(i, 0x100) { } { // while(i < 0x100)
x := add(x, mload(i))
i := add(i, 0x20)
}
}
函数声明
Yul允许定义函数。这些不应该与 Solidity 中的函数相混淆,因为它们从来不是一个合约的外部接口的一部分, 而是一个独立于 Solidity 函数的命名空间的一部分。
对于EVM来说,Yul函数从堆栈中获取它们的参数(和一个返回的PC), 同时也将结果放到堆栈中。用户定义的函数和内置函数的调用方式完全相同。
函数可以在任何地方定义,并且在它们所声明的块中是可见的。 在一个函数中,您不能访问在该函数之外定义的局部变量。
函数声明参数和返回变量,与Solidity类似。 为了返回一个值,您可以把它分配给返回变量。
如果您调用一个返回多个值的函数,
您必须用 a, b := f(x)
或 let a, b := f(x)
将它们分配给多个变量。
leave
语句可以用来退出当前函数。它的工作原理类似于其他语言中的 return
语句,
只是它不需要返回值,它只是退出函数,
函数将返回当前分配给返回变量的任何值。
注意,EVM语言有一个内置的函数叫 return
,
它可以退出整个执行环境(内部消息调用),而不仅仅是当前的yul函数。
下面的例子通过平方和乘法实现了幂函数。
{
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) }
}
}
}
Yul形式规范
本章正式描述Yul代码。Yul代码通常放置在Yul对象内, Yul对象将在它们自己的章节中解释。
代码块 = '{' 语句* '}'
语句 =
代码块 |
函数定义 |
变量声明 |
赋值 |
If |
表达式 |
Switch |
For 循环 |
循环中断 |
退出
函数定义 =
'function' 标识符 '(' 带类型的标识符列表? ')'
( '->' 带类型的标识符列表 )? 代码块
变量声明 =
'let' 带类型的标识符列表 ( ':=' 表达式 )?
赋值 =
标识符列表 ':=' 表达式
表达式 =
函数调用 | 标识符 | 字面量
If 条件语句 =
'if' 表达式 代码块
Switch 条件语句 =
'switch' 表达式 ( Case+ Default? | Default )
Case =
'case' 字面量 代码块
Default =
'default' 代码块
For 循环 =
'for' 代码块 表达式 代码块 代码块
循环中断 =
'break' | 'continue'
退出 = 'leave'
函数调用 =
标识符 '(' ( 表达式 ( ',' 表达式 )* )? ')'
标识符 = [a-zA-Z_$] [a-zA-Z_$0-9.]*
标识符列表 = 标识符 ( ',' 标识符)*
类型名 = 标识符
带类型的标识符列表 = 标识符 ( ':' 类型名 )? ( ',' 标识符 ( ':' 类型名 )? )*
字面量 =
(数字字面量 | 字符串字面量 | True字面量 | False字面量) ( ':' 类型名 )?
数字字面量 = 十六进制数字 | 十进制数字
字符串字面量 = '"' ([^"\r\n\\] | '\\' .)* '"'
True字面量 = 'true'
False字面量 = 'false'
十六进制数字 = '0x' [0-9a-fA-F]+
十进制数字 = [0-9]+
语法层面的限制
除语法直接规定的限制外,还适用以下限制:
Switch 语句必须至少有一个判断条件(包括默认条件)。
所有情况下的值都需要有相同的类型和不同的值。
如果表达式类型的所有可能值都被覆盖,则不允许有默认情况
(例如,一个带有 bool
表达式的Switch 语句,如果有一个真和一个假的情况,则不允许有默认情况)。
每个表达式都评估为零或多个值。 标识符和字面量精确地评估为一个值, 而函数调用求值为所调用函数的返回值。
在变量声明和赋值中,右边的表达式(如果存在的话)必须求值到与左边的变量数量相等的数值。 这是唯一允许对一个以上的值进行评估的表达式的情况。 在赋值或变量声明的左侧,同一个变量名称不能出现多次。
也是语句的表达式(即在块级)必须评估为零值。
在所有其他情况下,表达式必须精确评估为一个值。
continue
或 break
语句只能在for循环的主体内使用,如下所示。
考虑包含该语句的最内部循环。循环和语句必须在同一个函数中,或者两者必须在最高层。
该语句必须在循环的主体块中;不能在循环的初始化块或更新块中。
值得强调的是,这个限制只适用于包含 continue
或 break
语句的最内层循环:
这个最内层循环,以及 continue
或 break
语句,
可以出现在外层循环的任何地方,可能是外层循环的初始化块或更新块中。
例如,下面的例子是合法的,因为 break
出现在内循环的主体块中,尽管也出现在外循环的更新块中。
for {} true { for {} true {} { break } }
{
}
for循环的条件部分必须精确评估为一个值。
leave
语句只能在一个函数内使用。
函数不能在for循环初始化块的任何地方定义。
字面量不可以大于它们本身的类型。已定义的最大类型宽度为 256 比特。
在赋值和函数调用过程中,各个值的类型必须匹配。 没有隐式的类型转换。一般来说,只有当EVM语言提供一个适当的内置函数, 接收一个类型的值并返回一个不同类型的值时,才能实现类型转换。
作用域规则
Yul中的作用域是与块联系在一起的(函数和for循环是例外,下面会解释),
所有的声明( 函数定义(FunctionDefinition)
, 变量声明(VariableDeclaration)
)
都将新的标识符引入这些作用域。
标识符在其定义的块中是可见的(包括所有子节点和子块)。
函数在整个块中是可见的(甚至在其定义之前),
而变量只在 变量声明
之后的语句中可见。
特别是,变量不能在其自身变量声明的右侧被引用。 函数可以在其声明之前就被引用(如果它们是可见的)。
作为一般范围规则的一个例外,for 循环的 “init” 部分(第一个块)的范围延伸到for 循环的所有其他部分。 这意味着在init部分声明的变量(和函数)(但不在init部分的块内)在for循环的所有其他部分都是可见的。
在for循环的其他部分声明的标识符要遵守常规的句法范围规则。
这意味着一个for循环的形式 for { I... } C { P... } { B... }
等同于
{ I... for {}. C { P... } { B... }
.
函数的参数和返回参数在函数体中是可见的,其名称必须是不同的。
在函数内部,不可能引用一个在该函数之外声明的变量。
影子变量是不允许的,也就是说,您不能在另一个同名的标识符也可见的地方声明一个标识符, 即使因为它是在当前函数之外声明的而不可能引用它。
形式规范
我们通过提供一个在AST的各个节点上重载的评估函数E来正式指定Yul。 由于内置函数可能有副作用,E接收两个状态对象和AST节点,并返回两个新的状态对象和数量不定的其他值。 这两个状态对象是全局状态对象(在EVM的背景下,它是区块链的内存、存储和状态) 和本地状态对象(本地变量的状态,即EVM中堆栈的一段)。
如果AST节点是一个语句,E返回两个状态对象和一个 “mode”,
该mode用于 break
, continue
和 leave
语句。
如果AST节点是一个表达式,E返回两个状态对象和表达式所评估的数值。
全局状态的确切性质在这个高层次的描述中没有明确说明。
本地状态 L
是标识符 i
到值 v
的映射,表示为 L[i] = v
。
对于标识符 v
, 我们用 $v
作为标识符的名字。
我们将为 AST 节点使用解构符号。
E(G, L, <{St1, ..., Stn}>: Block) =
let G1, L1, mode = E(G, L, St1, ..., Stn)
let L2 be a restriction of L1 to the identifiers of L
G1, L2, mode
E(G, L, St1, ..., Stn: Statement) =
if n is zero:
G, L, regular
else:
let G1, L1, mode = E(G, L, St1)
if mode is regular then
E(G1, L1, St2, ..., Stn)
otherwise
G1, L1, mode
E(G, L, FunctionDefinition) =
G, L, regular
E(G, L, <let var_1, ..., var_n := rhs>: VariableDeclaration) =
E(G, L, <var_1, ..., var_n := rhs>: Assignment)
E(G, L, <let var_1, ..., var_n>: VariableDeclaration) =
let L1 be a copy of L where L1[$var_i] = 0 for i = 1, ..., n
G, L1, regular
E(G, L, <var_1, ..., var_n := rhs>: Assignment) =
let G1, L1, v1, ..., vn = E(G, L, rhs)
let L2 be a copy of L1 where L2[$var_i] = vi for i = 1, ..., n
G1, L2, regular
E(G, L, <for { i1, ..., in } condition post body>: ForLoop) =
if n >= 1:
let G1, L1, mode = E(G, L, i1, ..., in)
// 由于语法限制,mode 必须是规则的
if mode is leave then
G1, L1 restricted to variables of L, leave
otherwise
let G2, L2, mode = E(G1, L1, for {} condition post body)
G2, L2 restricted to variables of L, mode
else:
let G1, L1, v = E(G, L, condition)
if v is false:
G1, L1, regular
else:
let G2, L2, mode = E(G1, L, body)
if mode is break:
G2, L2, regular
otherwise if mode is leave:
G2, L2, leave
else:
G3, L3, mode = E(G2, L2, post)
if mode is leave:
G3, L3, leave
otherwise
E(G3, L3, for {} condition post body)
E(G, L, break: BreakContinue) =
G, L, break
E(G, L, continue: BreakContinue) =
G, L, continue
E(G, L, leave: Leave) =
G, L, leave
E(G, L, <if condition body>: If) =
let G0, L0, v = E(G, L, condition)
if v is true:
E(G0, L0, body)
else:
G0, L0, regular
E(G, L, <switch condition case l1:t1 st1 ... case ln:tn stn>: Switch) =
E(G, L, switch condition case l1:t1 st1 ... case ln:tn stn default {})
E(G, L, <switch condition case l1:t1 st1 ... case ln:tn stn default st'>: Switch) =
let G0, L0, v = E(G, L, condition)
// i = 1 .. n
// 对字面量求值,上下文无关
let _, _, v1 = E(G0, L0, l1)
...
let _, _, vn = E(G0, L0, ln)
if there exists smallest i such that vi = v:
E(G0, L0, sti)
else:
E(G0, L0, st')
E(G, L, <name>: Identifier) =
G, L, L[$name]
E(G, L, <fname(arg1, ..., argn)>: FunctionCall) =
G1, L1, vn = E(G, L, argn)
...
G(n-1), L(n-1), v2 = E(G(n-2), L(n-2), arg2)
Gn, Ln, v1 = E(G(n-1), L(n-1), arg1)
Let <function fname (param1, ..., paramn) -> ret1, ..., retm block>
be the function of name $fname visible at the point of the call.
Let L' be a new local state such that
L'[$parami] = vi and L'[$reti] = 0 for all i.
Let G'', L'', mode = E(Gn, L', block)
G'', Ln, L''[$ret1], ..., L''[$retm]
E(G, L, l: StringLiteral) = G, L, str(l),
where str is the string evaluation function,
which for the EVM dialect is defined in the section 'Literals' above
E(G, L, n: HexNumber) = G, L, hex(n)
where hex is the hexadecimal evaluation function,
which turns a sequence of hexadecimal digits into their big endian value
E(G, L, n: DecimalNumber) = G, L, dec(n),
where dec is the decimal evaluation function,
which turns a sequence of decimal digits into their big endian value
EVM语言
目前Yul的默认语言是当前选择的EVM版本的EVM语言,与EVM的一个版本。
该语言中唯一可用的类型是 u256
,即Ethereum虚拟机的256位本地类型。
因为它是该语言的默认类型,所以可以省略。
下表列出了所有内置函数(取决于EVM版本),并提供了函数/操作码的语义的简短描述。 本文件并不想成为以太坊虚拟机的完整描述。如果您对精确的语义感兴趣,请参考另一份文件。
标有 -
的操作码不返回结果,所有其他操作码正好返回一个值。
标有 F
, H
, B
, C
, I
和 L
的操作码分别
是从Frontier,Homestead,Byzantium,Constantinople,Istanbul或London版本出现的。
在下文中, mem[a...b)
表示从位置 a
开始到不包括位置 b
的内存字节,
storage[p]
表示插槽 p
的存储内容。
由于Yul管理着局部变量和控制流,所以不能使用干扰这些功能的操作码。
这包括 dup
和 swap
指令,以及 jump
指令,标签和 push
指令。
指令 |
解释 |
||
---|---|---|---|
stop() |
- |
F |
停止执行,与return(0, 0)相同 |
add(x, y) |
F |
x + y |
|
sub(x, y) |
F |
x - y |
|
mul(x, y) |
F |
x * y |
|
div(x, y) |
F |
x / y 或 如果 y == 0,则为 0 |
|
sdiv(x, y) |
F |
x / y,对于有符号的二进制补数,如果 y == 0,则为 0 |
|
mod(x, y) |
F |
x % y, 如果 y == 0,则为 0 |
|
smod(x, y) |
F |
x % y, 对于有符号的二进制补数, 如果 y == 0,则为 0 |
|
exp(x, y) |
F |
x的y次方 |
|
not(x) |
F |
x的位 "非"(x的每一个位都被否定) |
|
lt(x, y) |
F |
如果 x < y,则为1,否则为0 |
|
gt(x, y) |
F |
如果 x > y,则为1,否则为0 |
|
slt(x, y) |
F |
如果 x < y,则为1,否则为0,适用于有符号的二进制数 |
|
sgt(x, y) |
F |
如果 x > y,则为1,否则为0,适用于有符号的二进制补数 |
|
eq(x, y) |
F |
如果 x == y,则为1,否则为0 |
|
iszero(x) |
F |
如果 x == 0,则为1,否则为0 |
|
and(x, y) |
F |
x 和 y 的按位 "与" |
|
or(x, y) |
F |
x 和 y 的按位 "或" |
|
xor(x, y) |
F |
x 和 y 的按位 "异或" |
|
byte(n, x) |
F |
x的第n个字节,其中最重要的字节是第0个字节 |
|
shl(x, y) |
C |
将 y 逻辑左移 x 位 |
|
shr(x, y) |
C |
将 y 逻辑右移 x 位 |
|
sar(x, y) |
C |
将 y 算术右移 x 位 |
|
addmod(x, y, m) |
F |
(x + y) % m,采用任意精度算术,如果m == 0则为0 |
|
mulmod(x, y, m) |
F |
(x * y) % m,采用任意精度算术,如果m == 0则为0 |
|
signextend(i, x) |
F |
从第 (i*8+7) 位开始进行符号扩展,从最低符号位开始计算 |
|
keccak256(p, n) |
F |
keccak(mem[p...(p+n))) |
|
pc() |
F |
代码中的当前位置 |
|
pop(x) |
- |
F |
丢弃值 x |
mload(p) |
F |
mem[p...(p+32)) |
|
mstore(p, v) |
- |
F |
mem[p...(p+32)) := v |
mstore8(p, v) |
- |
F |
mem[p] := v & 0xff ((只修改了一个字节)) |
sload(p) |
F |
storage[p] |
|
sstore(p, v) |
- |
F |
storage[p] := v |
msize() |
F |
内存的大小,即最大的访问内存索引 |
|
gas() |
F |
仍可以执行的气体值 |
|
address() |
F |
当前合约/执行环境的地址 |
|
balance(a) |
F |
地址为A的余额,以wei为单位 |
|
selfbalance() |
I |
相当于balance(address()),但更便宜 |
|
caller() |
F |
消息调用者(不包括 |
|
callvalue() |
F |
与当前调用一起发送的wei的数量 |
|
calldataload(p) |
F |
从位置p开始的调用数据(32字节) |
|
calldatasize() |
F |
调用数据的大小,以字节为单位 |
|
calldatacopy(t, f, s) |
- |
F |
从位置f的calldata复制s字节到位置t的内存中 |
codesize() |
F |
当前合约/执行环境的代码大小 |
|
codecopy(t, f, s) |
- |
F |
从位置f的code中复制s字节到位置t的内存中 |
extcodesize(a) |
F |
地址为a的代码的大小 |
|
extcodecopy(a, t, f, s) |
- |
F |
像codecopy(t, f, s)一样,但在地址a处取代码 |
returndatasize() |
B |
最后返回数据的大小 |
|
returndatacopy(t, f, s) |
- |
B |
从位置f的returndata复制s字节到位置t的内存中 |
extcodehash(a) |
C |
地址a的代码哈希值 |
|
create(v, p, n) |
F |
用代码mem[p...(p+n))创建新的合约,发送v数量的wei并返回新地址; 错误时返回0 |
|
create2(v, p, n, s) |
C |
在keccak256(0xff . this . s . keccak256(mem[p...(p+n)))地址处
创建代码为mem[p...(p+n)]的新合约
并发送v 数量个wei和返回新地址, 其中 |
|
call(g, a, v, in, insize, out, outsize) |
F |
调用地址 a 上的合约,以 mem[in..(in+insize)) 作为输入 一并发送 g 数量的 gas 和 v 数量的 wei, 以 mem[out..(out+outsize)) 作为输出空间。 若错误,返回 0 (比如,gas 用光) 若成功,返回 1 查看更多 |
|
callcode(g, a, v, in, insize, out, outsize) |
F |
相当于 |
|
delegatecall(g, a, in, insize, out, outsize) |
H |
相当于 |
|
staticcall(g, a, in, insize, out, outsize) |
B |
相当于 |
|
return(p, s) |
- |
F |
终止执行,返回 mem[p..(p+s)) 上的数据 |
revert(p, s) |
- |
B |
终止执行,恢复状态变更,返回 mem[p..(p+s)) 上的数据 |
selfdestruct(a) |
- |
F |
终止执行,销毁当前合约,并且将余额发送到地址 a |
invalid() |
- |
F |
以无效指令终止执行 |
log0(p, s) |
- |
F |
用 mem[p..(p+s)] 上的数据产生日志,但没有 topic |
log1(p, s, t1) |
- |
F |
用 mem[p..(p+s)] 上的数据和 topic t1 产生日志 |
log2(p, s, t1, t2) |
- |
F |
用 mem[p..(p+s)] 上的数据和 topic t1,t2 产生日志 |
log3(p, s, t1, t2, t3) |
- |
F |
用 mem[p..(p+s)] 上的数据和 topic t1,t2,t3 产生日志 |
log4(p, s, t1, t2, t3, t4) |
- |
F |
用 mem[p..(p+s)] 上的数据和 topic t1,t2,t3,t4 产生日志 |
chainid() |
I |
执行链的ID(EIP-1344) |
|
basefee() |
L |
当前区块的基本费用(EIP-3198和EIP-1559) |
|
origin() |
F |
交易发送者 |
|
gasprice() |
F |
交易的气体价格n |
|
blockhash(b) |
F |
区块编号b的哈希值--只针对最近的256个区块,不包括当前区块。 |
|
coinbase() |
F |
目前的挖矿的受益者 |
|
timestamp() |
F |
自 epoch 开始的,当前块的时间戳,以秒为单位 |
|
number() |
F |
当前区块号 |
|
difficulty() |
F |
当前区块的难度 |
|
gaslimit() |
F |
当前区块的区块 gas 限制 |
备注
call*
指令使用 out
和 outsize
参数来在内存中定义的一个区域,
用于放置返回或失败数据。这个区域的写入取决于被调用的合约返回多少字节。
如果它返回更多的数据,只有第一个 outsize
字节被写入。您可以使用 returndatacopy
操作码访问其余的数据。
如果它返回较少的数据,那么剩下的字节根本不被触及。
您需要使用 returndatacopy
操作码来检查这个内存区域的哪一部分包含返回数据。
剩下的字节将保留调用前的值。
在一些内部语言中,还有一些额外的函数:
datasize, dataoffset, datacopy
函数 datasize(x)
, dataoffset(x)
和 datacopy(t, f, l)
用来访问Yul对象的其他部分。
datasize
和 dataoffset
只能接受字符串字面量(其他对象的名称)作为参数,
并分别返回数据区的大小和偏移量。
对于EVM, datacopy
函数等同于 codecopy
。
setimmutable, loadimmutable
函数 setimmutable(offset, "name", value)
和 loadimmutable("name")
用于Solidity中的不可变机制,
不能很好地映射到纯Yul。
对 setimmutable(offset, "name", value)
的调用假定包含给定不可变的命名的合约的运行时代码
在偏移量 offset
处被复制到内存中,并将把 value
写到内存中的所有位置(相对于 offset
),
这些位置包含在运行时代码中为调用 loadimmutable("name")
产生的占位符。
linkersymbol
函数 linkersymbol("library_id")
是一个占位符,用来表示被链接器替换的地址字头。
它的第一个也是唯一的参数必须是一个字符串字面量,并且唯一地代表要插入的地址。
标识符可以是任意的,但是当编译器从Solidity源产生Yul代码时,它使用一个库名,
并以定义该库的源单元的名称作为限定。
要用一个特定的库地址链接代码,必须在命令行上的 --libraries
选项中提供相同的标识符。
例如,这段代码
let a := linkersymbol("file.sol:Math")
相当于
let a := 0x1234567890123456789012345678901234567890
当使用 --libraries "file.sol:Math=0x1234567890123456789012345678901234567890
选项调用链接器时。
请参阅 使用命令行编译器 以了解有关 Solidity 链接器的详情。
memoryguard
调用 let ptr := memoryguard(size)
的调用者(其中 size
必须是一个数字字面量)
承诺他们只使用 [0, size]
范围内的内存,或者从 ptr
开始的无界范围。
由于 memoryguard
调用的存在表明所有的内存访问都遵守这一限制,
它允许优化器执行额外的优化步骤,
例如堆栈限制规避器,它试图将原本无法到达的堆栈变量转移到内存中。
Yul优化器承诺只使用内存范围 [size, ptr)
来实现其目的。
如果优化器不需要保留任何内存,它认为 ptr == size
。
memoryguard
可以被多次调用,但是需要在一个Yul子对象内有相同的字样作为参数。
如果在一个子对象中发现至少一个 memoryguard
的调用,额外的优化步骤将在它身上运行。
verbatim
一组 verbatim...
内置函数可以让您为Yul编译器不知道的操作码创建字节码。
它还允许您创建不会被优化器修改的字节码序列。
这些函数是 verbatim_<n>i_<m>o("<data>", ...)
,其中
n
是一个介于0和99之间的小数,指定输入栈槽/变量的数量m
是一个介于0和99之间的小数,指定输出栈槽/变量的数量data
是一个字符串字面量,包含字节的序列
例如,如果您想定义一个函数,将输入值乘以2,而不需要优化器触及常数2,您可以使用
let x := calldataload(0)
let double := verbatim_1i_1o(hex"600202", x)
这段代码将产生一个 dup1
操作码来检索 x
(尽管优化器可能直接重新使用 calldataload
操作码的结果),
后面直接是 600202
。该代码被假定为消耗 x
的复制值,并在堆栈顶部产生结果。
然后编译器生成代码,为 double
分配一个堆栈槽,并将结果存储在那里。
与所有的操作码一样,参数被安排在堆栈中,最左边的参数在最上面, 而返回值则被假定是以最右边的变量在栈顶的方式排列的。
由于 verbatim
可以用来生成任意的操作码,甚至是Solidity编译器不知道的操作码,
在与优化器一起使用 verbatim
时,必须小心。
即使优化器被关闭,代码生成器也必须确定堆栈布局,这意味着,例如,
使用 verbatim
来修改堆栈高度会导致未定义行为。
下面是一个不完全的列表,列出了对逐字字节码的限制, 这些限制不被编译器检查。违反这些限制会导致未定义的行为。
控制流不应该跳入或跳出 verbatim 块,但它可以在同一个 verbatim 块内跳入。
除了输入和输出参数外,堆栈内容不应该被访问。
堆栈的高度差应该正好是
m - n
(输出槽减去输入槽)。Verbatim字节码不能对周围的字节码做任何假设。 所有需要的参数都必须作为堆栈变量传入。
优化器不分析 verbatim 字节码,总是假设它修改了状态的所有方面,
因此只能在 verbatim
函数调用中做很少的优化。
优化器将 verbatim 字节码视为一个不透明的代码块。它不会分割它, 但可能会移动、重复或与相同的 verbatim 字节码块结合。 如果一个 verbatim 的字节码块不能被控制流所触及。 它可以被删除。
警告
在讨论EVM的改进是否会破坏现有的智能合约时,
verbatim
内部的功能不能得到与Solidity编译器本身使用的功能一样的考虑。
备注
为了避免混淆,所有以字符串 verbatim
开头的标识符都被保留,
不能用于用户定义的标识符。
Yul对象的规范
Yul对象被用来分组命名代码和数据部分。
函数 datasize
, dataoffset
和 datacopy
可以用来从代码中访问这些部分。
十六进制字符串可用于指定十六进制编码的数据,
普通字符串为本地编码。对于代码,
datacopy
将访问其组装的二进制所表示的数据。
对象 = 'object' 字面量 '{' 代码 ( 对象 | 数据 )* '}'
代码 = 'code' 块
数据 = 'data' 字面量 ( 十六进制字面量 | 字面量 )
十六进制字面量 = 'hex' ('"' ([0-9a-fA-F]{2})* '"' | '\'' ([0-9a-fA-F]{2})* '\'')
字面量 = '"' ([^"\r\n\\] | '\\' .)* '"'
对于上面的 Block
,指的是前一章解释的Yul代码语法中的 Block
。
备注
当一个对象的名称以 _deployed
结尾时,Yul 优化器将其视为部署的代码。
这样做的唯一后果是优化器中的不同 gas 成本启发式算法。
备注
可以定义名称中包含 .
的数据对象或子对象,
但不可能通过 datasize
, dataoffset
或 datacopy
访问它们,
因为 .
是作为分隔符用来访问另一个对象内的对象。
备注
被称为 ".metadata"
的数据对象有特殊意义:
它不能从代码中访问,并且总是被附加到字节码的最末端,
无论它在对象中的位置如何。
其他具有特殊意义的数据对象在未来可能会被添加,
但它们的名字总是以 .
开头。
下面是一个Yul对象的例子:
// 一个合约由一个单一的对象组成,
// 其子对象代表要部署的代码或它可以创建的其他合约。
// 单个 “代码” 节点是该对象的可执行代码。
// 每一个(其他)命名的对象或数据部分都被序列化,
// 并被特殊的内置函数 datacopy / dataoffset / datasize 所访问
// 当前对象、子对象和当前对象内的数据项都在范围内。
object "Contract1" {
// 这是合约的构造函数代码。
code {
function allocate(size) -> ptr {
ptr := mload(0x40)
// 请注意,Solidity 生成的 IR 代码也保留了内存偏移量 ``0x60``,但一个纯 Yul 对象可以自由地使用内存。
if iszero(ptr) { ptr := 0x60 }
mstore(0x40, add(ptr, size))
}
// 首先创建 “Contract2”
let size := datasize("Contract2")
let offset := allocate(size)
// 这将转化为EVM的代码拷贝。
datacopy(offset, dataoffset("Contract2"), size)
// 构造函数参数是一个单一的数字 0x1234
mstore(add(offset, size), 0x1234)
pop(create(offset, add(size, 32), 0))
// 现在返回运行时对象
// 当前执行的代码是构造函数代码)。
size := datasize("Contract1_deployed")
offset := allocate(size)
// 这将变成 Ewasm 的 内存->内存 复制
// 和 EVM 的代码复制。
datacopy(offset, dataoffset("Contract1_deployed"), size)
return(offset, size)
}
data "Table2" hex"4123"
object "Contract1_deployed" {
code {
function allocate(size) -> ptr {
ptr := mload(0x40)
// 请注意,Solidity 生成的 IR 代码也保留了内存偏移量 ``0x60``,但一个纯 Yul 对象可以自由地使用内存。
if iszero(ptr) { ptr := 0x60 }
mstore(0x40, add(ptr, size))
}
// 运行时代码
mstore(0, "Hello, World!")
return(0, 0x20)
}
}
// 嵌入对象。使用情况是,外面是一个工厂合约,
// 而 Contract2 是由工厂创建的代码。
object "Contract2" {
code {
// 此处是代码 ...
}
object "Contract2_deployed" {
code {
// 此处是代码 ...
}
}
data "Table1" hex"4123"
}
}
Yul 优化器
Yul优化器对Yul代码进行操作,并对输入、输出和中间状态使用相同的语言。这使得优化器的调试和验证变得容易。
请参考一般的 优化器文档,以了解关于不同优化阶段和如何使用优化器的更多细节。
如果您想在独立的Yul模式下使用Solidity,您可以用 --optimize
激活优化器,
并可选择用 --optimize-runs
指定 预期合约执行次数:
solc --strict-assembly --optimize --optimize-runs 200
在Solidity模式下,Yul优化器与常规优化器一起被激活。
优化步骤顺序
默认情况下,Yul优化器将其预定义的优化步骤序列应用于生成的程序集。
您可以使用 --yul-optimizations
选项覆盖这个序列,并提供您自己的序列:
solc --optimize --ir-optimized --yul-optimizations 'dhfoD[xarrscLMcCTU]uljmul'
优化步骤的顺序很重要,会影响到输出的质量。
此外,应用一个步骤可能为其他已经应用的步骤发现新的优化机会,所以重复步骤往往是有益的。
通过用方括号( []
)包围序列的一部分,您告诉优化器重复应用该部分,
直到它不再改善优化结果的大小。您可以在一个序列中多次使用方括号,但它们不能被嵌套。
有以下优化步骤:
缩写 |
全称 |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
一些步骤依赖于 块展平器
, 函数分组器
, 循环初始重写器
所保证的属性。
由于这个原因,Yul优化器总是在应用用户提供的任何步骤之前应用它们。
基于推理的简化器 是一个优化器步骤,目前在默认步骤集中没有启用。 它使用一个SMT解算器来简化算术表达式和布尔条件。 它还没有得到彻底的测试或验证,可能会产生不可重现的结果, 所以请谨慎使用!
完整的ERC20示例(基于yul)
object "Token" {
code {
// 将创建者存储在零号槽中。
sstore(0, caller())
// 部署合约
datacopy(0, dataoffset("runtime"), datasize("runtime"))
return(0, datasize("runtime"))
}
object "runtime" {
code {
// 防止发送以太的保护措施
require(iszero(callvalue()))
// 调度器
switch selector()
case 0x70a08231 /* "balanceOf(address)" */ {
returnUint(balanceOf(decodeAsAddress(0)))
}
case 0x18160ddd /* "totalSupply()" */ {
returnUint(totalSupply())
}
case 0xa9059cbb /* "transfer(address,uint256)" */ {
transfer(decodeAsAddress(0), decodeAsUint(1))
returnTrue()
}
case 0x23b872dd /* "transferFrom(address,address,uint256)" */ {
transferFrom(decodeAsAddress(0), decodeAsAddress(1), decodeAsUint(2))
returnTrue()
}
case 0x095ea7b3 /* "approve(address,uint256)" */ {
approve(decodeAsAddress(0), decodeAsUint(1))
returnTrue()
}
case 0xdd62ed3e /* "allowance(address,address)" */ {
returnUint(allowance(decodeAsAddress(0), decodeAsAddress(1)))
}
case 0x40c10f19 /* "mint(address,uint256)" */ {
mint(decodeAsAddress(0), decodeAsUint(1))
returnTrue()
}
default {
revert(0, 0)
}
function mint(account, amount) {
require(calledByOwner())
mintTokens(amount)
addToBalance(account, amount)
emitTransfer(0, account, amount)
}
function transfer(to, amount) {
executeTransfer(caller(), to, amount)
}
function approve(spender, amount) {
revertIfZeroAddress(spender)
setAllowance(caller(), spender, amount)
emitApproval(caller(), spender, amount)
}
function transferFrom(from, to, amount) {
decreaseAllowanceBy(from, caller(), amount)
executeTransfer(from, to, amount)
}
function executeTransfer(from, to, amount) {
revertIfZeroAddress(to)
deductFromBalance(from, amount)
addToBalance(to, amount)
emitTransfer(from, to, amount)
}
/* ---------- calldata 解码函数 ----------- */
function selector() -> s {
s := div(calldataload(0), 0x100000000000000000000000000000000000000000000000000000000)
}
function decodeAsAddress(offset) -> v {
v := decodeAsUint(offset)
if iszero(iszero(and(v, not(0xffffffffffffffffffffffffffffffffffffffff)))) {
revert(0, 0)
}
}
function decodeAsUint(offset) -> v {
let pos := add(4, mul(offset, 0x20))
if lt(calldatasize(), add(pos, 0x20)) {
revert(0, 0)
}
v := calldataload(pos)
}
/* ---------- calldata 编码函数 ---------- */
function returnUint(v) {
mstore(0, v)
return(0, 0x20)
}
function returnTrue() {
returnUint(1)
}
/* -------- 事件 ---------- */
function emitTransfer(from, to, amount) {
let signatureHash := 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef
emitEvent(signatureHash, from, to, amount)
}
function emitApproval(from, spender, amount) {
let signatureHash := 0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925
emitEvent(signatureHash, from, spender, amount)
}
function emitEvent(signatureHash, indexed1, indexed2, nonIndexed) {
mstore(0, nonIndexed)
log3(0, 0x20, signatureHash, indexed1, indexed2)
}
/* -------- 存储布局 ---------- */
function ownerPos() -> p { p := 0 }
function totalSupplyPos() -> p { p := 1 }
function accountToStorageOffset(account) -> offset {
offset := add(0x1000, account)
}
function allowanceStorageOffset(account, spender) -> offset {
offset := accountToStorageOffset(account)
mstore(0, offset)
mstore(0x20, spender)
offset := keccak256(0, 0x40)
}
/* -------- 存储访问 ---------- */
function owner() -> o {
o := sload(ownerPos())
}
function totalSupply() -> supply {
supply := sload(totalSupplyPos())
}
function mintTokens(amount) {
sstore(totalSupplyPos(), safeAdd(totalSupply(), amount))
}
function balanceOf(account) -> bal {
bal := sload(accountToStorageOffset(account))
}
function addToBalance(account, amount) {
let offset := accountToStorageOffset(account)
sstore(offset, safeAdd(sload(offset), amount))
}
function deductFromBalance(account, amount) {
let offset := accountToStorageOffset(account)
let bal := sload(offset)
require(lte(amount, bal))
sstore(offset, sub(bal, amount))
}
function allowance(account, spender) -> amount {
amount := sload(allowanceStorageOffset(account, spender))
}
function setAllowance(account, spender, amount) {
sstore(allowanceStorageOffset(account, spender), amount)
}
function decreaseAllowanceBy(account, spender, amount) {
let offset := allowanceStorageOffset(account, spender)
let currentAllowance := sload(offset)
require(lte(amount, currentAllowance))
sstore(offset, sub(currentAllowance, amount))
}
/* ---------- 工具函数 ---------- */
function lte(a, b) -> r {
r := iszero(gt(a, b))
}
function gte(a, b) -> r {
r := iszero(lt(a, b))
}
function safeAdd(a, b) -> r {
r := add(a, b)
if or(lt(r, a), lt(r, b)) { revert(0, 0) }
}
function calledByOwner() -> cbo {
cbo := eq(owner(), caller())
}
function revertIfZeroAddress(addr) {
require(addr)
}
function require(condition) {
if iszero(condition) { revert(0, 0) }
}
}
}
}