Yul
Yul(以前也称为julia或iulia)是一种中间语言,可以为不同的后端编译为字节码。
计划支持EVM 1.0、EVM 1.5和Ewasm,并将其设计为所有三个平台的可用公分母。它已经可以在独立模式下使用,并且可以用于Solidity内部的“内联汇编”,并且有一个使用YUL作为中间语言的Solidity编译器的实验实现。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专门用于不同目标平台和功能集的需求。
目前,榆方言只有一种。这种方言使用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)
}
}
}
在 end of the section ,可以找到ERC-20标准的完整实施。
独立使用
可以使用Solidity编译器在EVM方言中以其独立形式使用Yul。这将使用 Yul object notation 因此可以将代码作为数据引用以部署契约。此Yul模式可用于命令行编译器(使用 --strict-assembly
)为了 standard-json interface :
{
"language": "Yul",
"sources": { "input.yul": { "content": "{ sstore(0, 1) }" } },
"settings": {
"outputSelection": { "*": { "*": ["*"], "": [ "*" ] } },
"optimizer": { "enabled": true, "details": { "yul": true } }
}
}
警告
Yul正在积极开发中,字节码生成只在Yul的EVM方言中以evm1.0为目标完全实现。
Yul的非正式描述
在下面,我们将讨论Yul语言的各个方面。在示例中,我们将使用默认的EVM方言。
句法
Yul以与Solidity相同的方式解析注释、文本和标识符,因此您可以使用 //
和 /* */
表示评论。有一个例外:Yul中的标识符可以包含点: .
.
Yul可以指定由代码、数据和子对象组成的“对象”。请看 Yul Objects 下面是关于这方面的详细信息。在本节中,我们只关注这样一个对象的代码部分。此代码部分始终由大括号分隔的块组成。大多数工具只支持在预期的对象所在位置指定代码块。
在代码块中,可以使用以下元素(有关详细信息,请参阅后面的部分):
文字,即
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 mload(0) case 0 {{ revert() }} default {{ mstore(0, 1) }}
对于循环,例如。
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,然后按位 and
字符串“abc”进行计算。最后的值赋给一个名为的局部变量 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语句的扩展版本。它获取一个表达式的值并将其与几个文本常量进行比较。取与匹配常量对应的分支。与其他编程语言相反,出于安全原因,控制流不会从一种情况延续到下一种情况。可能有一个后备或默认情况 default
如果没有一个文本常量匹配,则取该值。
{
let x := 0
switch calldataload(4)
case 0 {
x := calldataload(0x24)
}
default {
x := calldataload(0x44)
}
sstore(0, div(x, 2))
}
箱子的列表没有用大括号括起来,但是箱子的主体确实需要它们。
循环
Yul支持由包含初始化部分、条件、迭代后部分和正文的头组成的循环。条件必须是表达式,而其他三个是块。如果初始化部分在顶层声明任何变量,则这些变量的范围将扩展到循环的所有其他部分。
这个 break
和 continue
语句可以在正文中分别用于退出循环或跳到post部分。
下面的示例计算内存中某个区域的和。
{
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]+
对语法的限制
除了语法直接施加的限制外,以下限制适用:
开关必须至少有一个大小写(包括默认大小写)。所有大小写值都需要具有相同的类型和不同的值。如果表达式类型的所有可能值都被覆盖,则不允许使用默认大小写(即 bool
同时具有true和false大小写的表达式不允许使用默认大小写)。
每个表达式的计算结果为零或多个值。标识符和文字的计算结果正好是一个值,而函数调用计算的值数等于所调用函数的返回变量数。
在变量声明和赋值中,右侧表达式(如果存在)的计算结果必须等于左侧变量的数量。这是唯一允许表达式求值为多个值的情况。同一个变量名不能在赋值或变量声明的左侧出现多次。
同时也是语句的表达式(即在块级别)必须计算为零值。
在所有其他情况下,表达式的计算结果只能是一个值。
这个 continue
和 break
语句只能在循环体中使用,并且必须与循环处于相同的函数中(或者两者都必须处于顶层)。这个 continue
和 break
语句不能在循环的其他部分使用,即使在第二个循环的主体中限定作用域时也是如此。
for循环的条件部分必须精确计算为一个值。
这个 leave
语句只能在函数内部使用。
不能在for loop init块内的任何地方定义函数。
文字不能大于其类型。定义的最大类型为256位宽。
在赋值和函数调用期间,相应值的类型必须匹配。没有隐式类型转换。通常,只有当方言提供了一个适当的内置函数,该函数接受一个类型的值并返回另一个类型的值时,才能实现类型转换。
范围界定规则
yul中的范围绑定到块(异常是函数和for循环,如下所述)和所有声明 (FunctionDefinition
, VariableDeclaration
)在这些范围中引入新的标识符。
标识符在挡路中可见(包括所有子节点和子块):函数在整个挡路中可见(甚至在其定义之前),而变量仅从 VariableDeclaration
。
具体地说,变量不能在其自身变量声明的右侧引用。函数可以在其声明之前已经被引用(如果它们可见的话)。
作为一般作用域规则的例外,for循环的“init”部分(第一个挡路)的作用域扩展到for循环的所有其他部分。这意味着在init部分中声明的变量(和函数)(但不在init部分内的挡路中)在for循环的所有其他部分中都可见。
在for循环的其他部分声明的标识符遵循常规的语法作用域规则。
这意味着窗体的for循环 for {{ I... }} C {{ P... }} {{ B... }}
等于 {{ I... for {{}} C {{ P... }} {{ B... }} }}
.
函数的参数和返回参数在函数体中可见,它们的名称必须不同。
在函数内部,不能引用在该函数外部声明的变量。
不允许隐藏,即不能在另一个同名标识符也可见的位置声明标识符,即使无法引用该标识符,因为它是在当前函数外部声明的。
形式规范
我们通过在AST的各个节点上提供一个重载的求值函数E来正式指定Yul。由于内置函数可能有副作用,E接受两个state对象和AST节点,并返回两个新的state对象和可变数量的其他值。这两个状态对象是全局状态对象(在EVM上下文中是区块链的内存、存储和状态)和局部状态对象(局部变量的状态,即EVM中堆栈的一段)。
如果AST节点是一个语句,那么E返回两个状态对象和一个“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
G, L2, regular
E(G, L, <for { i1, ..., in } condition post body>: ForLoop) =
if n >= 1:
let G1, L, 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:
G2, 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, utf8EncodeLeftAligned(l),
where utf8EncodeLeftAligned performs a UTF-8 encoding of l
and aligns it left into 32 bytes
E(G, L, n: HexNumber) = G, L, hex(n)
where hex is the hexadecimal decoding function
E(G, L, n: DecimalNumber) = G, L, dec(n),
where dec is the decimal decoding function
EVM方言
当前Yul的默认方言是当前选定版本EVM的EVM方言。有一个版本的EVM。这种方言中唯一可用的类型是 u256
,以太坊虚拟机的256位本机类型。因为它是这个方言的默认类型,所以可以省略它。
下表列出了所有内置函数(取决于EVM版本),并提供了函数/操作码语义的简短描述。本文档不希望是以太坊虚拟机的完整描述。如果您对确切的语义感兴趣,请参阅其他文档。
操作码标记为 -
不返回结果,所有其他结果只返回一个值。操作码标记为 F
, H
, B
, C
, I
和 L
分别来自边疆、宅基地、拜占庭、君士坦丁堡、伊斯坦布尔或伦敦。
在以下内容中, mem[a...b)
表示从位置开始的内存字节数 a
不包括职位 b
和 storage[p]
表示插槽中的存储内容 p
.
由于Yul管理局部变量和控制流,因此干扰这些特性的操作码不可用。这包括 dup
和 swap
说明以及 jump
说明、标签和 push
指令。
说明 |
解释 |
||
---|---|---|---|
停止() |
- |
F |
停止执行,与返回(0,0)相同 |
添加(x,y) |
F |
X+Y |
|
子(x,y) |
F |
X-Y轴 |
|
mul(x,y) |
F |
X*Y |
|
分区(X,Y) |
F |
x/y或0,如果y==0 |
|
SDIV(X,Y) |
F |
x/y,对于2的补码中的有符号数,如果y==0,则为0 |
|
模(X,Y) |
F |
x%y,如果y==0,则为0 |
|
SMOD(x,y) |
F |
x%y,对于2的补码中的有符号数字,如果y==0,则为0 |
|
经验(x,y) |
F |
x到y的幂 |
|
不是(X) |
F |
x的位“not”(x的每一位都被否定) |
|
左(X,Y) |
F |
如果x<y,则为1,否则为0 |
|
gt(x,y) |
F |
如果x>y,则为1,否则为0 |
|
SLT(X,Y) |
F |
如果x<y,则为1,否则为0,表示补码为2的有符号数。 |
|
SGT(X,Y) |
F |
如果x>y,则为1,否则为0,表示补码为2的有符号数。 |
|
公式(x,y) |
F |
如果x==y,则为1,否则为0 |
|
零(X) |
F |
如果x=0,则为1,否则为0 |
|
和(x,y) |
F |
x和y的“与”位 |
|
或(x,y) |
F |
x和y的“或”位 |
|
xor(x,y) |
F |
x和y的位“异或” |
|
字节(N,X) |
F |
x的第n个字节,其中最重要的字节是第0个字节 |
|
SHL(X,Y) |
C |
逻辑左Y偏移x位 |
|
SHR(x,y)型 |
C |
逻辑右移Y x位 |
|
SAR(x,y) |
C |
有符号算术右移y x位 |
|
加模(X,Y,M) |
F |
(x+y)%m,任意精度算术,如果m==0,则为0 |
|
穆尔莫德(X,Y,M) |
F |
(x*y)%m,任意精度算术,如果m==0,则为0 |
|
签名扩展(i,x) |
F |
符号从(i*8+7)第位开始扩展,从最低有效位开始计数 |
|
凯卡256(P,N) |
F |
凯卡(Mem[P…(P+N))) |
|
pc() |
F |
代码中的当前位置 |
|
弹出(X) |
- |
F |
丢弃值x |
负荷(P) |
F |
内存[P…(P+32) |
|
存储(P,V) |
- |
F |
内存[P…(P+32)):=V |
M存储8(P,V) |
- |
F |
微机电 [p] :=v&0xff(仅修改单个字节) |
斯洛德(P) |
F |
存储 [p] |
|
存储(P,V) |
- |
F |
存储 [p] :=V |
msize() |
F |
内存大小,即最大访问内存索引 |
|
气体() |
F |
可供执行的气体 |
|
地址() |
F |
当前合同/执行上下文的地址 |
|
余额(A) |
F |
地址A的Wei余额 |
|
自我平衡() |
I |
相当于balance(address()),但更便宜 |
|
调用者() |
F |
呼叫发送方(不包括 |
|
调用值() |
F |
小薇随同当前的电话 |
|
调用数据加载(P) |
F |
从位置P开始调用数据(32字节) |
|
调用数据大小() |
F |
呼叫数据的字节大小 |
|
呼叫数据复制(T、F、S) |
- |
F |
将位置f处的calldata的s字节复制到位置t处的mem |
代码大小() |
F |
当前合同/执行上下文的代码大小 |
|
编解码器复制(T、F、S) |
- |
F |
将S字节从F位置的代码复制到T位置的MEM |
外部代码大小(A) |
F |
地址A的代码大小 |
|
extcodecopy(A、T、F、S) |
- |
F |
像codecopy(t,f,s),但在地址A处获取代码 |
returndatasize() |
B |
上次返回数据的大小 |
|
返回数据复制(T、F、S) |
- |
B |
将S字节从F位置的返回数据复制到T位置的MEM |
外部代码哈希(A) |
C |
地址A的代码哈希 |
|
创建(V、P、N) |
F |
使用代码mem[p.(p+n))新建合同,并发送v wei并返回新地址,出错时返回0 |
|
创建2(V、P、N、S) |
C |
使用代码mem[p.(p+n))在地址keccak256(0xff)创建新合同。这个。s.keccak256(mem[p.(p+n)并发送v wei并返回新地址,其中 |
|
呼叫(G、A、V、In、Insize、Out、特大) |
F |
呼叫地址A的合同,输入MEM[输入…(输入+输入),提供G气体和V气体,输出区域MEM[输出…(输出+超大),出错时返回0(如输出气体),成功时返回1 See more |
|
呼叫码(G、A、V、In、Insize、Out、特大) |
F |
相同的 |
|
代表呼叫(G、A、In、Insize、Out、特大) |
H |
相同的 |
|
静态调用(g,a,in,insize,out,extize) |
B |
相同的 |
|
返回(P,S) |
- |
F |
结束执行,返回数据内存[P…(P+S) |
还原(P,S) |
- |
B |
结束执行,还原状态更改,返回数据内存[P…(P+S) |
自毁(A) |
- |
F |
结束执行,销毁当前合同并向 |
无效() |
- |
F |
以无效指令结束执行 |
对数0(P,S) |
- |
F |
不带主题和数据内存的日志[P…(P+S) |
日志1(P、S、T1) |
- |
F |
用主题T1和数据MEM[P…(P+S)记录 |
对数2(P、S、T1、T2) |
- |
F |
记录主题T1、T2和数据MEM[P…(P+S)] |
log3(P、S、T1、T2、T3) |
- |
F |
记录主题T1、T2、T3和数据内存[P…(P+S)) |
log4(P、S、T1、T2、T3、T4) |
- |
F |
记录主题T1、T2、T3、T4和数据内存[P…(P+S)) |
链ID() |
I |
执行链ID(EIP-1344) |
|
basefee() |
L |
当前挡路基础费用(弹性公网IP-3198和弹性公网IP-1559) |
|
原点() |
F |
交易发送方 |
|
加斯普里斯() |
F |
交易天然气价格 |
|
块哈希(B) |
F |
块nr b的哈希-仅限于最近256个块(不包括当前块) |
|
coinbase() |
F |
当前采矿受益人 |
|
时间戳() |
F |
当前块的时间戳(以自epoch以来的秒为单位) |
|
编号() |
F |
当前块号 |
|
难度() |
F |
当前块的难度 |
|
气体极限() |
F |
当前区块的区块气限值 |
注解
这个 call*
说明使用 out
和 outsize
参数来定义内存中放置返回或故障数据的区域。此区域的写入取决于调用的约定返回的字节数。如果它返回更多数据,则只返回第一个 outsize
写入字节。您可以使用访问数据的睡觉 returndatacopy
操作码。如果它返回的数据较少,则剩余的字节根本不会被触及。您需要使用 returndatasize
用于检查此内存区的哪一部分包含返回数据的操作码。其余字节将保留调用前的值。
在一些内部方言中,还有其他功能:
datasize、dataoffset、datacopy
功能 datasize(x)
, dataoffset(x)
和 datacopy(t, f, l)
用于访问Yul对象的其他部分。
datasize
和 dataoffset
只能将字符串文字(其他对象的名称)作为参数,并分别返回数据区域中的大小和偏移量。对于EVM来说 datacopy
功能相当于 codecopy
.
设置不可变,加载不可变
功能 setimmutable(offset, "name", value)
和 loadimmutable("name")
用于固体中的不可变机制,并且不能很好地映射到纯YL。调用 setimmutable(offset, "name", value)
假定包含给定命名不可变的约定的运行时代码在偏移量处复制到内存中 offset
并将写下 value
到内存中的所有位置(相对于 offset
),它包含为调用生成的占位符 loadimmutable("name")
在运行时代码中。
链接符号
函数 linkersymbol("fq_library_name")
是要由链接器替换的地址文本的占位符。它的第一个也是唯一的参数必须是字符串文本,并表示与 --libraries
选择权。
例如这个代码
let a := linkersymbol("file.sol:Math")
等于
let a := 0x1234567890123456789012345678901234567890
使用调用链接器时 --libraries "file.sol:Math=0x1234567890123456789012345678901234567890
选项。
见 Using the Commandline Compiler 有关Solidity链接器的详细信息。
存储保护
这个函数在EVM方言中有对象。的呼叫者 let ptr := memoryguard(size)
(何处) size
必须是一个字面数字)承诺他们只使用两个范围内的内存 [0, size)
或者无限范围从 ptr
.
因为 memoryguard
call表示所有内存访问都遵守此限制,它允许优化器执行附加的优化步骤,例如堆栈限制规避器,它试图移动内存中无法访问的堆栈变量。
Yul优化器承诺只使用内存范围 [size, ptr)
就其目的而言。如果优化器不需要保留任何内存,它将保存 ptr == size
.
memoryguard
可以多次调用,但需要在一个Yul子对象中具有与参数相同的文本。如果至少有一个 memoryguard
调用在子对象中找到时,将对其运行其他优化程序步骤。
逐字记录
一组 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
可以用来生成任意操作码,甚至是固态编译器未知的操作码,所以在使用 verbatim
与优化器一起使用。即使在优化器关闭时,代码生成器也必须确定堆栈布局,这意味着例如使用 verbatim
修改堆栈高度可能会导致未定义的行为。
以下是编译器未检查的逐字节码限制的非详尽列表。违反这些限制可能会导致不确定的行为。
控制流不应该逐字跳入或跳出逐字块,但它可以在相同的逐字挡路内跳跃。
不应访问除输入和输出参数之外的堆栈内容。
堆叠高度差应该精确到
m - n
(输出插槽减去输入插槽)。逐字节码不能对周围的字节码做出任何假设。所有必需的参数都必须作为堆栈变量传入。
优化器不会逐字分析字节码,并且始终假定它修改了状态的所有方面,因此只能在以下方面执行极少的优化 verbatim
函数调用。
优化器将逐字节码视为不透明的挡路代码。它不会拆分它,但可能会将其与完全相同的逐字节码块一起移动、复制或组合。如果控制流无法到达逐字节码挡路,则可以将其删除。
警告
在讨论EVM改进是否会破坏现有智能合同的过程中, verbatim
不能收到与固态编译器本身使用的相同的考虑事项。
注解
为避免念力,所有以字符串开头的标识符 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
指 Block
在上一章解释的yul代码语法中。
注解
名称包含 .
can be defined but it is not possible to access them through datasize
, dataoffset
or datacopy
because .
用作访问另一个对象内的对象的分隔符。
注解
名为的数据对象 ".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)
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(offset, add(size, 32), 0))
// now return the runtime object (the currently
// executing code is the constructor code)
size := datasize("runtime")
offset := allocate(size)
// This will turn into a memory->memory copy for Ewasm and
// a codecopy for EVM
datacopy(offset, dataoffset("runtime"), size)
return(offset, size)
}
data "Table2" hex"4123"
object "runtime" {
code {
function allocate(size) -> ptr {
ptr := mload(0x40)
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 "runtime" {
code {
// code here ...
}
}
data "Table1" hex"4123"
}
}
Yul优化器
Yul优化器对Yul代码进行操作,并对输入、输出和中间状态使用相同的语言。这样可以方便地调试和验证优化器。
请向总指挥咨询 optimizer documentation 有关不同优化阶段以及如何使用优化器的更多详细信息。
如果您想在独立的YUL模式中使用SOLIDITY,可以使用以下命令激活优化器 --optimize
并选择性地指定 expected number of contract executions 使用 --optimize-runs
:
solc --strict-assembly --optimize --optimize-runs 200
在Solidity模式下,Yul优化器与常规优化器一起激活。
优化步长序列
默认情况下,Yul优化器将其预定义的优化步骤序列应用于生成的程序集。您可以重写此序列并使用 --yul-optimizations
选项:
solc --optimize --ir-optimized --yul-optimizations 'dhfoD[xarrscLMcCTU]uljmul'
步骤的顺序非常重要,并且会影响输出的质量。此外,应用一个步骤可能会为已经应用的其他步骤发现新的优化机会,因此重复步骤通常是有益的。通过将部分序列括在方括号中 ([]
)告诉优化器重复应用该零件,直到它不再改善生成的部件的大小。可以在一个序列中多次使用方括号,但不能嵌套。
以下优化步骤可用:
缩写 |
全名 |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
某些步骤取决于 BlockFlattener
, FunctionGrouper
, ForLoopInitRewriter
. 因此,Yul优化器总是在应用用户提供的任何步骤之前应用它们。
ReasoningBasedSimplifier是一个优化器步骤,当前在默认步骤集中未启用。它使用SMT解算器来简化算术表达式和布尔条件。它还没有经过彻底的测试或验证,并且可能产生不可重复的结果,所以请小心使用!
完整的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) }
}
}
}
}