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 正在积极开发中,字节码生成仅在 Yul 的 EVM 方言中完全实现,目标为 EVM 1.0。
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))
除非是默认类型,否则字面量的类型必须在冒号后指定:
// 这将无法编译(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 语句可用于有条件地执行代码。 不能定义 “else” 块。如果需要多个选择,请考虑使用 “switch”(见下文)。
if lt(calldatasize(), 4) { revert(0, 0) }
主体的大括号是必需的。
开关
你可以使用 switch 语句作为 if 语句的扩展版本。
它获取表达式的值并将其与多个字面常量进行比较。
对应于匹配常量的分支被采用。
与其他编程语言不同,为了安全起见,控制流不会从一个 case 继续到下一个。
可以有一个称为 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 对象中,这在它们自己的章节中进行了说明。
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]+
语法限制
除了语法直接施加的限制外,以下限制适用:
Switch 必须至少有一个 case(包括默认 case)。
所有 case 值需要具有相同的类型和不同的值。
如果表达式类型的所有可能值都被覆盖,则不允许有默认 case(即,具有 true 和 false case 的 bool
表达式的 switch 不允许有默认 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 循环的所有其他部分。 这意味着在初始化部分声明的变量(和函数)(但不在初始化部分的块内)在 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 方言。此方言中唯一可用的类型是 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
指令。
备注
call*
指令使用 out
和 outsize
参数来定义一个内存区域,
返回或失败数据将被放置在该区域。根据被调用合约返回的字节数,该区域会被写入。
如果返回更多数据,则仅写入前 outsize
字节。你可以使用 returndatacopy
操作码访问其余数据。
如果返回的数据较少,则剩余字节将完全不被触及。
你需要使用 returndatasize
操作码来检查该内存区域的哪一部分包含返回数据。
剩余字节将保留调用前的值。
备注
在 EVM 版本 >= Paris 中不允许使用 difficulty()
指令。
随着 Paris 网络升级,之前称为 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
写入内存中所有
相对于 offset
的位置,这些位置包含为运行时代码中对 loadimmutable("name")
调用生成的占位符。
linkersymbol
函数 linkersymbol("library_id")
是一个占位符,用于由链接器替换的地址字面量。
它的第一个也是唯一的参数必须是字符串字面量,并唯一表示要插入的地址。
标识符可以是任意的,但当编译器从 Solidity 源代码生成 Yul 代码时,
它使用带有定义该库的源单元名称的库名称进行限定。
要将代码与特定库地址链接,必须在命令行的 --libraries
选项中提供相同的标识符。
例如,这段代码
let a := linkersymbol("file.sol:Math")
在链接器使用 --libraries "file.sol:Math=0x1234567890123456789012345678901234567890
选项时
等同于
let a := 0x1234567890123456789012345678901234567890
有关 Solidity 链接器的详细信息,请参见 Using the Commandline Compiler。
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
是一个包含字节序列的字符串字面量
例如,如果你想定义一个将输入乘以二的函数,而不让优化器触碰常量二,你可以使用
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
函数调用之间进行非常少的优化。
优化器将 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 对象的示例:
// 合约由一个单一对象组成,子对象表示要部署的代码或它可以创建的其他合约。
// 单一的 "code" 节点是对象的可执行代码。
// 每个(其他)命名对象或数据部分都被序列化并
// 使其可通过特殊的内置函数 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 的 codecopy
datacopy(offset, dataoffset("Contract2"), size)
// 构造函数参数是单个数字 0x1234
mstore(add(offset, size), 0x1234)
pop(create(0, offset, add(size, 32)))
// 现在返回运行时对象(当前执行的代码是构造函数代码)
size := datasize("Contract1_deployed")
offset := allocate(size)
// 这将转变为 EVM 的 codecopy
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 代码上运行,并使用相同的语言进行输入、输出和中间状态。这使得调试和验证优化器变得简单。
有关不同优化阶段及如何使用优化器的更多详细信息,请参阅一般的 optimizer documentation。
如果你想在独立的 Yul 模式下使用 Solidity,可以使用 --optimize
激活优化器,并可选地使用 --optimize-runs
指定 expected number of contract executions:
solc --strict-assembly --optimize --optimize-runs 200
在 Solidity 模式下,Yul 优化器与常规优化器一起激活。
优化步骤序列
有关优化序列的详细信息以及缩写列表,请参阅 optimizer docs。
完整的 ERC20 示例
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) }
}
}
}
}