Yul
Yul(以前也称为 JULIA 或 IULIA)是一种中间语言,可以编译为不同后端的字节码。
它可以在独立模式下使用,也可以在 Solidity 中用于“内联汇编”。编译器在基于 IR 的代码生成器(“新代码生成”或“基于 IR 的代码生成”)中使用 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 循环而不是递归来实现相同的函数。这里,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 标准的完整实现。
独立使用
您可以在 EVM 方言中使用独立形式的 Yul,使用 Solidity 编译器。这将使用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 位来自第一个字节,即字节以大端形式解释。十六进制字符串首先被视为字节序列,将每对相邻的十六进制数字视为一个字节。字节序列不能超过 32 个字节(即 64 个十六进制数字),并按上述方式处理。
在为 EVM 编译时,这将被转换为适当的 PUSHi
指令。在以下示例中,3
和 2
相加得到 5,然后对字符串“abc”进行按位 and
运算。最终值被分配给一个名为 x
的局部变量。
上述 32 字节限制不适用于传递给需要字面量参数的内置函数的字符串字面量(例如 setimmutable
或 loadimmutable
)。这些字符串永远不会出现在生成的字节码中。
let x := and("abc", add(3, 2))
除非是默认类型,否则字面量的类型必须在冒号后指定。
// This will not compile (u32 and u256 type not implemented yet)
let x := and("abc":u32, add(3:u256, 2:u256))
函数调用
内置函数和用户定义函数(见下文)都可以像前面的示例一样调用。如果函数返回单个值,则它可以直接在表达式中再次使用。如果它返回多个值,则必须将它们分配给局部变量。
function f(x, y) -> a, b { /* ... */ }
mstore(0x80, add(mload(0x80), 3))
// Here, the user-defined function `f` returns two values.
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 is "deallocated" here
sstore(v, zero)
} // v and zero are "deallocated" here
如果声明的变量应具有与默认类型不同的类型,则您在冒号后表示该类型。当您从返回多个值的函数调用赋值时,您也可以在一个语句中声明多个变量。
// This will not compile (u32 and u256 type not implemented yet)
{
let zero:u32 := 0:u32
let v:u256, t:u32 := f()
let x, y := g()
}
根据优化器设置,编译器可以在变量最后一次使用后立即释放堆栈槽,即使它仍在作用域内。
赋值
变量可以在定义后使用 :=
运算符进行赋值。可以同时为多个变量赋值。为此,值的个数和类型必须匹配。如果您想将从具有多个返回值参数的函数返回的值赋值,则必须提供多个变量。同一个变量不能在赋值的左侧多次出现,例如 x, x := f()
是无效的。
let v := 0
// re-assign v
v := 2
let t := add(v, 2)
function f() -> a, b { }
// assign multiple values
v, t := f()
如果
if 语句可用于有条件地执行代码。不能定义“else”块。如果您需要多个备选方案,请考虑使用“switch”(见下文)。
if lt(calldatasize(), 4) { revert(0, 0) }
主体的大括号是必需的。
开关
您可以使用 switch 语句作为 if 语句的扩展版本。它获取表达式的值并将其与多个字面量常量进行比较。将执行与匹配的常量对应的分支。与其他编程语言不同,出于安全原因,控制流不会从一个 case 继续到下一个 case。可以有一个称为 default
的备选方案或默认情况,如果所有字面量常量都不匹配,则会执行该情况。
{
let x := 0
switch calldataload(4)
case 0 {
x := calldataload(0x24)
}
default {
x := calldataload(0x44)
}
sstore(0, div(x, 2))
}
case 列表没有用大括号括起来,但 case 的主体需要大括号。
循环
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 函数。
以下示例通过平方和乘法实现 power 函数。
{
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 对象中,这些对象将在它们自己的章节中进行解释。
Block = '{' Statement* '}'
Statement =
Block |
FunctionDefinition |
VariableDeclaration |
Assignment |
If |
Expression |
Switch |
ForLoop |
BreakContinue |
Leave
FunctionDefinition =
'function' Identifier '(' TypedIdentifierList? ')'
( '->' TypedIdentifierList )? Block
VariableDeclaration =
'let' TypedIdentifierList ( ':=' Expression )?
Assignment =
IdentifierList ':=' Expression
Expression =
FunctionCall | Identifier | Literal
If =
'if' Expression Block
Switch =
'switch' Expression ( Case+ Default? | Default )
Case =
'case' Literal Block
Default =
'default' Block
ForLoop =
'for' Block Expression Block Block
BreakContinue =
'break' | 'continue'
Leave = 'leave'
FunctionCall =
Identifier '(' ( Expression ( ',' Expression )* )? ')'
Identifier = [a-zA-Z_$] [a-zA-Z_$0-9.]*
IdentifierList = Identifier ( ',' Identifier)*
TypeName = Identifier
TypedIdentifierList = Identifier ( ':' TypeName )? ( ',' Identifier ( ':' TypeName )? )*
Literal =
(NumberLiteral | StringLiteral | TrueLiteral | FalseLiteral) ( ':' TypeName )?
NumberLiteral = HexNumber | DecimalNumber
StringLiteral = '"' ([^"\r\n\\] | '\\' .)* '"'
TrueLiteral = 'true'
FalseLiteral = 'false'
HexNumber = '0x' [0-9a-fA-F]+
DecimalNumber = [0-9]+
语法限制
除了语法直接强加的限制之外,还适用以下限制。
开关必须至少有一个 case(包括默认 case)。所有 case 值必须具有相同的类型和不同的值。如果覆盖了表达式的所有可能的值,则不允许使用默认 case(即具有 bool
表达式且同时具有 true 和 false case 的开关不允许使用默认 case)。
每个表达式计算为零个或多个值。标识符和字面量计算为一个值,函数调用计算为一个值,该值的个数等于调用的函数的返回值变量的个数。
在变量声明和赋值中,右侧表达式(如果存在)必须计算为一个值,该值的个数等于左侧变量的个数。这是唯一允许表达式计算为一个以上值的场景。同一个变量名不能在赋值的左侧或变量声明中出现多次。
也是语句(即在块级)的表达式必须计算为零个值。
在所有其他情况下,表达式必须计算为一个值。
一个 continue
或 break
语句只能用在 for 循环的主体中,如下所示。考虑包含语句的最内层循环。循环和语句必须在同一个函数中,或者两者都必须在顶层。语句必须在循环的主体块中;它不能在循环的初始化块或更新块中。值得强调的是,此限制仅适用于包含 continue
或 break
语句的最内层循环:这个最内层循环,因此 continue
或 break
语句,可能出现在外层循环中的任何位置,可能出现在外层循环的初始化块或更新块中。例如,以下是合法的,因为 break
出现在内层循环的主体块中,尽管它也出现在外层循环的更新块中
for {} true { for {} true {} { break } }
{
}
for 循环的条件部分必须计算为一个值。
只能在函数内部使用 leave
语句。
函数不能定义在 for 循环初始化块中的任何位置。
字面量不能大于它们的类型。定义的最大类型为 256 位。
在赋值和函数调用中,相应值的类型必须匹配。没有隐式类型转换。一般情况下,类型转换只有在方言提供适当的内置函数才能实现,该函数接受一种类型的值并返回另一种类型的值。
作用域规则
Yul 中的作用域与块绑定(函数和 for 循环除外,如下所述),所有声明(FunctionDefinition
、VariableDeclaration
)将新的标识符引入这些作用域。
标识符在其定义的块中可见(包括所有子节点和子块):函数在整个块中可见(即使在其定义之前),而变量仅从VariableDeclaration
之后的语句开始可见。
特别是,变量不能在其自身变量声明的右侧引用。函数可以在其声明之前引用(如果它们可见)。
作为一般作用域规则的例外,for 循环的“init”部分(第一个块)的作用域扩展到 for 循环的所有其他部分。这意味着在 init 部分声明的变量(和函数)(但在 init 部分内的块内没有声明)在 for 循环的所有其他部分都可见。
在 for 循环的其他部分中声明的标识符遵循常规的语法作用域规则。
这意味着形式为 for { I... } C { P... } { B... }
的 for 循环等效于 { I... for {} C { P... } { B... } }
。
函数的参数和返回值参数在函数体内可见,它们的名字必须不同。
在函数内部,不能引用在该函数外部声明的变量。
不允许遮蔽,即,您不能在另一个同名标识符可见的地方声明一个标识符,即使由于它是在当前函数外部声明的,因此无法引用它。
正式规范
我们通过提供一个在 AST 的各个节点上重载的评估函数 E 来正式指定 Yul。由于内置函数可能具有副作用,因此 E 接受两个状态对象和 AST 节点,并返回两个新的状态对象和可变数量的其他值。这两个状态对象是全局状态对象(在 EVM 上下文中是区块链的内存、存储和状态)和局部状态对象(局部变量的状态,即 EVM 中堆栈的一部分)。
如果 AST 节点是语句,则 E 返回两个状态对象和一个“模式”,该模式用于 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 has to be regular or leave due to the syntactic restrictions
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
// Evaluate literals, context doesn't matter
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 版本。此方言中唯一可用的类型是 u256
,即以太坊虚拟机 256 位本机类型。由于它是该方言的默认类型,因此可以省略。
下表列出了所有内置函数(取决于 EVM 版本),并简要描述了函数/操作码的语义。本文档并非要完整描述以太坊虚拟机。如果您有兴趣了解精确的语义,请参阅其他文档。
用 -
标记的操作码不返回值,所有其他操作码都只返回一个值。用 F
、H
、B
、C
、I
、L
、P
和 N
标记的操作码分别从 Frontier、Homestead、Byzantium、Constantinople、Istanbul、London、Paris 或 Cancun 开始存在。
在下文中,mem[a...b)
表示从位置 a
开始到但不包括位置 b
的内存字节,storage[p]
表示槽 p
的存储内容,类似地,transientStorage[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 或 0 如果 y == 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 |
tload(p) |
N |
transientStorage[p] |
|
tstore(p, v) |
- |
N |
transientStorage[p] := v |
msize() |
F |
内存的大小,即访问过的最大内存索引 |
|
gas() |
F |
执行仍然可用的 gas |
|
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 |
将 s 个字节从 calldata 位置 f 复制到 mem 位置 t |
codesize() |
F |
当前合约/执行上下文的代码大小 |
|
codecopy(t, f, s) |
- |
F |
将 s 个字节从代码位置 f 复制到 mem 位置 t |
extcodesize(a) |
F |
地址 a 的代码大小 |
|
extcodecopy(a, t, f, s) |
- |
F |
类似于 codecopy(t, f, s) 但获取地址 a 的代码 |
returndatasize() |
B |
最后一个 returndata 的大小 |
|
returndatacopy(t, f, s) |
- |
B |
将 s 个字节从 returndata 位置 f 复制到 mem 位置 t |
mcopy(t, f, s) |
- |
N |
将 s 个字节从 mem 位置 f 复制到 mem 位置 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 |
使用输入 mem[in…(in+insize)) 在地址 a 处调用合约,提供 g gas 和 v wei 以及输出区域 mem[out…(out+outsize)),如果出错(例如,gas 不足)则返回 0,如果成功则返回 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)) |
log1(p, s, t1) |
- |
F |
使用主题 t1 记录数据 mem[p…(p+s)) |
log2(p, s, t1, t2) |
- |
F |
使用主题 t1, t2 记录数据 mem[p…(p+s)) |
log3(p, s, t1, t2, t3) |
- |
F |
使用主题 t1, t2, t3 记录数据 mem[p…(p+s)) |
log4(p, s, t1, t2, t3, t4) |
- |
F |
使用主题 t1, t2, t3, t4 记录数据 mem[p…(p+s)) |
chainid() |
I |
正在执行的链的 ID(EIP-1344) |
|
basefee() |
L |
当前区块的基本费用(EIP-3198 和 EIP-1559) |
|
blobbasefee() |
N |
当前区块的 blob 基本费用(EIP-7516 和 EIP-4844) |
|
origin() |
F |
交易发送者 |
|
gasprice() |
F |
交易的燃气价格 |
|
blockhash(b) |
F |
区块号 b 的哈希值 - 仅适用于最后 256 个区块,不包括当前区块 |
|
blobhash(i) |
N |
交易第 i 个 blob 的版本化哈希值 |
|
coinbase() |
F |
当前挖矿受益者 |
|
timestamp() |
F |
自纪元以来的当前区块的以秒为单位的时间戳 |
|
number() |
F |
当前区块号 |
|
difficulty() |
F |
当前区块的难度(参见下面的说明) |
|
prevrandao() |
P |
信标链提供的随机数(参见下面的说明) |
|
gaslimit() |
F |
当前区块的区块燃气限制 |
注意
call*
指令使用 out
和 outsize
参数来定义内存中放置返回值或失败数据的区域。此区域将根据被调用合约返回的字节数进行写入。如果它返回更多数据,则仅写入前 outsize
个字节。可以使用 returndatacopy
操作码访问其余数据。如果它返回的数据更少,则剩余字节将完全保持原样。需要使用 returndatasize
操作码来检查此内存区域的哪一部分包含返回值。剩余字节将保留它们在调用之前的数值。
注意
在 EVM 版本 >= Paris 中不允许使用 difficulty()
指令。随着巴黎网络升级,以前称为 difficulty
的指令的语义已更改,并且指令被重命名为 prevrandao
。它现在可以在完整的 256 位范围内返回任意值,而 Ethash 中记录的最高难度值约为 54 位。此更改在 EIP-4399 中有描述。请注意,与编译器中选择的 EVM 版本无关,指令的语义取决于最终的部署链。
警告
从版本 0.8.18 及更高版本开始,在 Solidity 和 Yul 中使用 selfdestruct
将触发弃用警告,因为 SELFDESTRUCT
操作码最终将如 EIP-6049 中所述在行为上发生重大更改。
在某些内部方言中,存在其他函数
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
写入内存中所有包含为调用 loadimmutable("name")
在运行时代码中生成的占位符的位置(相对于 offset
)。
linkersymbol
函数 linkersymbol("library_id")
是一个地址字面量的占位符,由链接器替换。它的第一个也是唯一的参数必须是字符串字面量,并唯一地表示要插入的地址。标识符可以是任意的,但当编译器从 Solidity 源代码生成 Yul 代码时,它使用限定了定义该库的源单元名称的库名称。要将代码与特定库地址链接,必须在命令行上的 --libraries
选项中提供相同的标识符。
例如,这段代码
let a := linkersymbol("file.sol:Math")
等效于
let a := 0x1234567890123456789012345678901234567890
当链接器使用 --libraries "file.sol:Math=0x1234567890123456789012345678901234567890
选项调用时。
有关 Solidity 链接器的详细信息,请参阅 使用命令行编译器。
memoryguard
此函数在具有对象的 EVM 方言中可用。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 块,但它可以在同一个 verbatim 块内跳跃。
不应访问除输入和输出参数之外的堆栈内容。
堆栈高度差应正好是
m - n
(输出槽位减去输入槽位)。Verbatim 字节码不能对周围字节码做出任何假设。所有必需的参数都必须作为堆栈变量传递。
优化器不会分析 verbatim 字节码,并且始终假设它修改了状态的所有方面,因此只能对 verbatim
函数调用进行很少的优化。
优化器将逐字字节码视为不透明的代码块。它不会拆分它,但可能会移动、复制或将它与相同的逐字字节码块组合。如果逐字字节码块无法通过控制流访问,则可以将其删除。
警告
在讨论 EVM 改进是否会破坏现有智能合约时,verbatim
内部的功能不能与 Solidity 编译器本身使用的功能享有同等考虑。
注意
为了避免混淆,所有以字符串 verbatim
开头的标识符都是保留的,不能用于用户定义的标识符。
Yul 对象规范
Yul 对象用于对命名代码和数据部分进行分组。函数 datasize
、dataoffset
和 datacopy
可用于从代码内部访问这些部分。十六进制字符串可用于以十六进制编码指定数据,常规字符串以本机编码指定。对于代码,datacopy
将访问其组装的二进制表示形式。
Object = 'object' StringLiteral '{' Code ( Object | Data )* '}'
Code = 'code' Block
Data = 'data' StringLiteral ( HexLiteral | StringLiteral )
HexLiteral = 'hex' ('"' ([0-9a-fA-F]{2})* '"' | '\'' ([0-9a-fA-F]{2})* '\'')
StringLiteral = '"' ([^"\r\n\\] | '\\' .)* '"'
上面,Block
指的是 Yul 代码语法中的 Block
,如上一章所述。
注意
以 _deployed
结尾的名称的对象将被 Yul 优化器视为已部署的代码。这唯一的区别是优化器中不同的 gas 成本启发式算法。
注意
名称包含 .
的数据对象或子对象可以定义,但无法通过 datasize
、dataoffset
或 datacopy
访问它们,因为 .
用作分隔符以访问另一个对象内部的对象。
注意
名为 ".metadata"
的数据对象具有特殊含义:它无法从代码中访问,并且始终附加到字节码的末尾,无论它在对象中的位置如何。
将来可能会添加其他具有特殊意义的数据对象,但它们的名称始终以 .
开头。
下面显示了一个 Yul 对象示例
// A contract consists of a single object with sub-objects representing
// the code to be deployed or other contracts it can create.
// The single "code" node is the executable code of the object.
// Every (other) named object or data section is serialized and
// made accessible to the special built-in functions datacopy / dataoffset / datasize
// The current object, sub-objects and data items inside the current object
// are in scope.
object "Contract1" {
// This is the constructor code of the contract.
code {
function allocate(size) -> ptr {
ptr := mload(0x40)
// Note that Solidity generated IR code reserves memory offset ``0x60`` as well, but a pure Yul object is free to use memory as it chooses.
if iszero(ptr) { ptr := 0x60 }
mstore(0x40, add(ptr, size))
}
// first create "Contract2"
let size := datasize("Contract2")
let offset := allocate(size)
// This will turn into codecopy for EVM
datacopy(offset, dataoffset("Contract2"), size)
// constructor parameter is a single number 0x1234
mstore(add(offset, size), 0x1234)
pop(create(0, offset, add(size, 32)))
// now return the runtime object (the currently
// executing code is the constructor code)
size := datasize("Contract1_deployed")
offset := allocate(size)
// This will turn into a codecopy for 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)
// Note that Solidity generated IR code reserves memory offset ``0x60`` as well, but a pure Yul object is free to use memory as it chooses.
if iszero(ptr) { ptr := 0x60 }
mstore(0x40, add(ptr, size))
}
// runtime code
mstore(0, "Hello, World!")
return(0, 0x20)
}
}
// Embedded object. Use case is that the outside is a factory contract,
// and Contract2 is the code to be created by the factory
object "Contract2" {
code {
// code here ...
}
object "Contract2_deployed" {
code {
// code here ...
}
}
data "Table1" hex"4123"
}
}
Yul 优化器
Yul 优化器对 Yul 代码进行操作,并使用相同的语言进行输入、输出和中间状态。这使得优化器的调试和验证变得容易。
有关不同优化阶段和如何使用优化器的更多详细信息,请参阅一般的 优化器文档。
如果要以独立 Yul 模式使用 Solidity,请使用 --optimize
激活优化器,并使用 --optimize-runs
选项指定 预期的合约执行次数
solc --strict-assembly --optimize --optimize-runs 200
在 Solidity 模式下,Yul 优化器与常规优化器一起激活。
优化步骤顺序
有关优化顺序以及缩写列表的详细信息,请参阅 优化器文档。
完整的 ERC20 示例
object "Token" {
code {
// Store the creator in slot zero.
sstore(0, caller())
// Deploy the contract
datacopy(0, dataoffset("runtime"), datasize("runtime"))
return(0, datasize("runtime"))
}
object "runtime" {
code {
// Protection against sending Ether
require(iszero(callvalue()))
// Dispatcher
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 decoding functions ----------- */
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 encoding functions ---------- */
function returnUint(v) {
mstore(0, v)
return(0, 0x20)
}
function returnTrue() {
returnUint(1)
}
/* -------- events ---------- */
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)
}
/* -------- storage layout ---------- */
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)
}
/* -------- storage access ---------- */
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))
}
/* ---------- utility functions ---------- */
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) }
}
}
}
}