Yul¶
Yul (先前被也被称为 JULIA 或 IULIA)是一种可以编译到各种不同后端的中间语言( 以太坊虚拟机 1.0,以太坊虚拟机 1.5,而 eWASM 也在计划中)。 正因为如此,它被设计成为这三种平台的可用的共同标准。 它已经可以用于 Solidity 内部的“内联汇编”,并且未来版本的 Solidity 编译器甚至会将 Yul 用作中间语言。 为 Yul 构建高级的优化器阶段也将会很容易。
备注
请注意,用于“内联汇编”的书写风格是不带类型的(所有的都是 u256
),内置函数与 以太坊虚拟机 操作码相同。
有关详细信息,请参阅内联汇编文档。
Yul 的核心组件是函数,代码块,变量,字面量,for 循环,if 条件语句,switch 条件语句,表达式和变量赋值。
Yul 是强类型的,变量和字面量都需要通过前缀符号来指明类型。支持的类型有:bool
, u8
, s8
, u32
, s32
,
u64
, s64
, u128
, s128
, u256
和 s256
。
Yul 本身甚至不提供操作符。如果目标平台是 以太坊虚拟机,则操作码将作为内置函数提供,但如果后端平台发生了变化,则可以重新实现它们。 有关强制性的内置函数的列表,请参阅下面的章节。
以下示例程序假定 以太坊虚拟机 操作码 mul
,div
和 mo
是原生支持或可以作为函数用以计算指数的。
{
function power(base:u256, exponent:u256) -> result:u256
{
switch exponent
case 0:u256 { result := 1:u256 }
case 1:u256 { result := base }
default:
{
result := power(mul(base, base), div(exponent, 2:u256))
switch mod(exponent, 2:u256)
case 1:u256 { result := mul(base, result) }
}
}
}
也可用 for 循环代替递归来实现相同的功能。这里,我们需要 以太坊虚拟机 操作码 lt
(小于)和 add
可用。
{
function power(base:u256, exponent:u256) -> result:u256
{
result := 1:u256
for { let i := 0:u256 } lt(i, exponent) { i := add(i, 1:u256) }
{
result := mul(result, base)
}
}
}
Yul 语言说明¶
本章介绍 Yul 代码。Yul 代码通常放置在一个 Yul 对象中,它将在下一节中介绍。
语法:
代码块 = '{' 语句* '}'
语句 =
代码块 |
函数定义 |
变量声明 |
赋值 |
表达式 |
Switch |
For 循环 |
循环中断
函数定义 =
'function' 标识符 '(' 带类型的标识符列表? ')'
( '->' 带类型的标识符列表 )? 代码块
变量声明 =
'let' 带类型的标识符列表 ( ':=' 表达式 )?
赋值 =
标识符列表 ':=' 表达式
表达式 =
函数调用 | 标识符 | 字面量
If 条件语句 =
'if' 表达式 代码块
Switch 条件语句 =
'switch' 表达式 Case* ( 'default' 代码块 )?
Case =
'case' 字面量 代码块
For 循环 =
'for' 代码块 表达式 代码块 代码块
循环中断 =
'break' | 'continue'
函数调用 =
标识符 '(' ( 表达式 ( ',' 表达式 )* )? ')'
标识符 = [a-zA-Z_$] [a-zA-Z_0-9]*
标识符列表 = 标识符 ( ',' 标识符)*
类型名 = 标识符 | 内置的类型名
内置的类型名 = 'bool' | [us] ( '8' | '32' | '64' | '128' | '256' )
带类型的标识符列表 = 标识符 ':' 类型名 ( ',' 标识符 ':' 类型名 )*
字面量 =
(数字字面量 | 字符串字面量 | 十六进制字面量 | True字面量 | False字面量) ':' 类型名
数字字面量 = 十六进制数字 | 十进制数字
十六进制字面量 = 'hex' ('"' ([0-9a-fA-F]{2})* '"' | '\'' ([0-9a-fA-F]{2})* '\'')
字符串字面量 = '"' ([^"\r\n\\] | '\\' .)* '"'
True字面量 = 'true'
False字面量 = 'false'
十六进制数字 = '0x' [0-9a-fA-F]+
十进制数字 = [0-9]+
语法层面的限制¶
Switches 必须至少有一个 case(包括 default )。
如果表达式的所有可能值都被覆盖了,那么不应该允许使用 default
(即带 bool
表达式的 switch 语句同时具有 true case 和 false case 的情况下不应再有 default 语句)。
每个表达式都求值为零个或多个值。 标识符和字面量求值为一个值,函数调用求值为所调用函数的返回值。
在变量声明和赋值中,右侧表达式(如果存在)求值后,必须得出与左侧变量数量相等的值。 这是唯一允许求值出多个值的表达式。
那种同时又是语句的表达式(即在代码块的层次)求值结果必须只有零个值。
在其他所有情况中,表达式求值后必须仅有一个值。
continue
和 break
语句只能用在循环体中,并且必须与循环处于同一个函数中(或者两者都必须在顶层)。
for 循环的条件部分的求值结果只能为一个值。
字面量不可以大于它们本身的类型。已定义的最大类型宽度为 256 比特。
作用域规则¶
Yul 中的作用域是与块(除了函数和 for 循环,如下所述)和所有引入新的标识符到作用域中的声明
( FunctionDefinition
,VariableDeclaration
)紧密绑定的。
标识符在将其定义的块中可见(包括所有子节点和子块)。 作为例外,for 循环的 “init” 部分中(第一个块)定义的标识符在 for 循环的所有其他部分(但不在循环之外)中都是可见的。 在 for 循环的其他部分声明的标识符遵守常规的作用域语法规则。 函数的参数和返回参数在函数体中可见,并且它们的名称不能相同。
变量只能在声明后引用。 尤其是,变量不能在它们自己的变量声明的右边被引用。 函数可以在声明之前被引用(如果它们是可见的)。
Shadowing 是不被允许的,即是说,你不能在同名标识符已经可见的情况下又定义该标识符,即使它是不可访问的。
在函数内,不可能访问声明在函数外的变量。
形式规范¶
我们通过在 AST 的各个节点上提供重载的求值函数 E 来正式指定 Yul。 任何函数都可能有副作用,所以 E 接受两个状态对象和 AST 节点作为它的参数,并返回两个新的状态对象和数量可变的其他值。
这两个状态对象是全局状态对象(在 以太坊虚拟机 的上下文中是 内存,存储 和区块链的状态)和本地状态对象(局部变量的状态,即 以太坊虚拟机 中堆栈的某个段)。 如果 AST 节点是一个语句,E 将返回两个状态对象和一个用于 break 和 continue 语句的 “mode”。 如果 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 var1, ..., varn := rhs>: VariableDeclaration) =
E(G, L, <var1, ..., varn := rhs>: Assignment)
E(G, L, <let var1, ..., varn>: VariableDeclaration) =
let L1 be a copy of L where L1[$vari] = 0 for i = 1, ..., n
G, L1, regular
E(G, L, <var1, ..., varn := rhs>: Assignment) =
let G1, L1, v1, ..., vn = E(G, L, rhs)
let L2 be a copy of L1 where L2[$vari] = vi for i = 1, ..., n
G, 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 必须是规则的
let G2, L2, mode = E(G1, L1, for {} condition post body)
// 由于语法限制,mode 必须是规则的
let L3 be the restriction of L2 to only variables of L
G2, L3, regular
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
else:
G3, L3, mode = E(G2, L2, post)
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, <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: HexLiteral) = G, L, hexString(l),
where hexString decodes l from hex and left-aligns it into 32 bytes
E(G, L, l: StringLiteral) = G, L, utf8EncodeLeftAligned(l),
where utf8EncodeLeftAligned performs a utf8 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
类型转换函数¶
Yul 不支持隐式类型转换,因此存在提供显式转换的函数。 在将较大类型转换为较短类型时,如果发生溢出,则可能会发生运行时异常。
- 下列类型的“截取式”转换是允许的:
bool
u32
u64
u256
s256
这里的每种类型的转换函数都有一个格式为 <input_type>to<output_type>(x:<input_type>) -> y:<output_type>
的原型,
比如 u32tobool(x:u32) -> y:bool
、u256tou32(x:u256) -> y:u32
或 s256tou256(x:s256) -> y:u256
。
备注
u32tobool(x:u32) -> y:bool
可以由 y := not(iszerou256(x))
实现,并且
booltou32(x:bool) -> y:u32
可以由 switch x case true:bool { y := 1:u32 } case false:bool { y := 0:u32 }
实现
低级函数¶
以下函数必须可用:
逻辑操作 |
|
not(x:bool) -> z:bool |
逻辑非 |
and(x:bool, y:bool) -> z:bool |
逻辑与 |
or(x:bool, y:bool) -> z:bool |
逻辑或 |
xor(x:bool, y:bool) -> z:bool |
异或 |
算术操作 |
|
addu256(x:u256, y:u256) -> z:u256 |
x + y |
subu256(x:u256, y:u256) -> z:u256 |
x - y |
mulu256(x:u256, y:u256) -> z:u256 |
x * y |
divu256(x:u256, y:u256) -> z:u256 |
x / y |
divs256(x:s256, y:s256) -> z:s256 |
x / y, 有符号数用补码形式 |
modu256(x:u256, y:u256) -> z:u256 |
x % y |
mods256(x:s256, y:s256) -> z:s256 |
x % y, 有符号数用补码形式 |
signextendu256(i:u256, x:u256) -> z:u256 |
从第 (i*8+7) 位开始进行符号扩展,从最低符号位开始计算 |
expu256(x:u256, y:u256) -> z:u256 |
x 的 y 次方 |
addmodu256(x:u256, y:u256, m:u256) -> z:u256 |
任意精度的数学模运算 (x + y) % m |
mulmodu256(x:u256, y:u256, m:u256) -> z:u256 |
任意精度的数学模运算 (x * y) % m |
ltu256(x:u256, y:u256) -> z:bool |
若 x < y 为 true, 否则为 false |
gtu256(x:u256, y:u256) -> z:bool |
若 x > y 为 true, 否则为 false |
sltu256(x:s256, y:s256) -> z:bool |
若 x < y 为 true, 否则为 false 有符号数用补码形式 |
sgtu256(x:s256, y:s256) -> z:bool |
若 x > y 为 true, 否则为 false 有符号数用补码形式 |
equ256(x:u256, y:u256) -> z:bool |
若 x == y 为 true, 否则为 false |
iszerou256(x:u256) -> z:bool |
若 x == 0 为 true, 否则为 false |
notu256(x:u256) -> z:u256 |
~x, 对 x 按位非 |
andu256(x:u256, y:u256) -> z:u256 |
x 和 y 按位与 |
oru256(x:u256, y:u256) -> z:u256 |
x 和 y 按位或 |
xoru256(x:u256, y:u256) -> z:u256 |
x 和 y 按位异或 |
shlu256(x:u256, y:u256) -> z:u256 |
将 x 逻辑左移 y 位 |
shru256(x:u256, y:u256) -> z:u256 |
将 x 逻辑右移 y 位 |
saru256(x:u256, y:u256) -> z:u256 |
将 x 算术右移 y 位 |
byte(n:u256, x:u256) -> v:u256 |
x 的第 n 字节,这里的索引位置是从 0 开始的; 能否用 and256(shr256(n, x), 0xff) 来替换它, 并使它在 EVM 后端之外被优化呢? |
内存和存储 |
|
mload(p:u256) -> v:u256 |
mem[p..(p+32)) |
mstore(p:u256, v:u256) |
mem[p..(p+32)) := v |
mstore8(p:u256, v:u256) |
mem[p] := v & 0xff - 仅修改单个字节 |
sload(p:u256) -> v:u256 |
storage[p] |
sstore(p:u256, v:u256) |
storage[p] := v |
msize() -> size:u256 |
内存的大小, 即已访问过的内存的最大下标, 因为内存扩展的限制(只能按字进行扩展) 返回值永远都是 32 字节的倍数 |
执行控制 |
|
create(v:u256, p:u256, s:u256) |
以 mem[p..(p+s)) 上的代码创建一个新合约,发送 v 个 wei,并返回一个新的地址 |
call(g:u256, a:u256, v:u256, in:u256, insize:u256, out:u256, outsize:u256) -> r:u256 |
调用地址 a 上的合约,以 mem[in..(in+insize)) 作为输入 一并发送 g gas 和 v wei ,以 mem[out..(out+outsize)) 作为输出空间。若错误,返回 0 (比如,gas 用光 成功,返回 1 |
callcode(g:u256, a:u256, v:u256, in:u256, insize:u256, out:u256, outsize:u256) -> r:u256 |
相当于 |
delegatecall(g:u256, a:u256, in:u256, insize:u256, out:u256, outsize:u256) -> r:u256 |
相当于 |
abort() |
终止 (相当于EVM上的非法指令) |
return(p:u256, s:u256) |
终止执行,返回 mem[p..(p+s)) 上的数据 |
revert(p:u256, s:u256) |
终止执行,恢复状态变更,返回 mem[p..(p+s)) 上的数据 |
selfdestruct(a:u256) |
终止执行,销毁当前合约,并且将余额发送到地址 a |
log0(p:u256, s:u256) |
用 mem[p..(p+s)] 上的数据产生日志,但没有 topic |
log1(p:u256, s:u256, t1:u256) |
用 mem[p..(p+s)] 上的数据和 topic t1 产生日志 |
log2(p:u256, s:u256, t1:u256, t2:u256) |
用 mem[p..(p+s)] 上的数据和 topic t1,t2 产生日志 |
log3(p:u256, s:u256, t1:u256, t2:u256, t3:u256) |
用 mem[p..(p+s)] 上的数据和 topic t1,t2,t3 产生日志 |
log4(p:u256, s:u256, t1:u256, t2:u256, t3:u256, t4:u256) |
用 mem[p..(p+s)] 上的数据和 topic t1,t2,t3,t4 产生日志 |
状态查询 |
|
blockcoinbase() -> address:u256 |
当前的矿工 |
blockdifficulty() -> difficulty:u256 |
当前区块的难度 |
blockgaslimit() -> limit:u256 |
当前区块的区块 gas 限制 |
blockhash(b:u256) -> hash:u256 |
区块号为 b 的区块的哈希, 仅可用于最近的 256 个区块,不包含当前区块 |
blocknumber() -> block:u256 |
当前区块号 |
blocktimestamp() -> timestamp:u256 |
自 epoch 开始的,当前块的时间戳,以秒为单位 |
txorigin() -> address:u256 |
交易的发送方 |
txgasprice() -> price:u256 |
交易中的 gas 价格 |
gasleft() -> gas:u256 |
还可用于执行的 gas |
balance(a:u256) -> v:u256 |
地址 a 上的 wei 余额 |
this() -> address:u256 |
当前合约/执行上下文的地址 |
caller() -> address:u256 |
调用的发送方 (不包含委托调用) |
callvalue() -> v:u256 |
与当前调用一起发送的 wei |
calldataload(p:u256) -> v:u256 |
从 position p 开始的 calldata (32 字节) |
calldatasize() -> v:u256 |
以字节为单位的 calldata 的大小 |
calldatacopy(t:u256, f:u256, s:u256) |
从位置为 f 的 calldata 中,拷贝 s 字节到内存位置 t |
codesize() -> size:u256 |
当前合约/执行上下文的代码大小 |
codecopy(t:u256, f:u256, s:u256) |
从 code 位置 f 拷贝 s 字节到内存位置 t |
extcodesize(a:u256) -> size:u256 |
地址 a 上的代码大小 |
extcodecopy(a:u256, t:u256, f:u256, s:u256) |
相当于 codecopy(t, f, s),但从地址 a 获取代码 |
其他 |
|
discard(unused:bool) |
丢弃值 |
discardu256(unused:u256) |
丢弃值 |
|
将一个 u256 拆分为四个 u64 |
|
将四个 u64 组合为一个 u256 |
keccak256(p:u256, s:u256) -> v:u256 |
keccak(mem[p…(p+s))) |
后端¶
后端或目标负责将 Yul 翻译到特定字节码。 每个后端都可以暴露以后端名称为前缀的函数。 我们为两个建议的后端保留 evm_
和 ewasm_
前缀。
后端: EVM¶
目标 以太坊虚拟机 将具有所有用 evm_ 前缀暴露的 以太坊虚拟机 底层操作码。
后端: “EVM 1.5”¶
TBD
后端: eWASM¶
TBD
Yul 对象说明¶
语法:
顶层对象 = 'object' '{' 代码? ( 对象 | 数据 )* '}'
对象 = 'object' 字符串字面量 '{' 代码? ( 对象 | 数据 )* '}'
代码 = 'code' 代码块
数据 = 'data' 字符串字面量 十六进制字面量
十六进制字面量 = 'hex' ('"' ([0-9a-fA-F]{2})* '"' | '\'' ([0-9a-fA-F]{2})* '\'')
字符串字面量 = '"' ([^"\r\n\\] | '\\' .)* '"'
在上面,代码块
指的是前一章中解释的 Yul 代码语法中的 代码块
。
Yul 对象示例如下:
..code:
// 代码由单个对象组成。 单个 “code” 节点是对象的代码。
// 每个(其他)命名的对象或数据部分都被序列化
// 并可供特殊内置函数:datacopy / dataoffset / datasize 用于访问
object {
code {
let size = datasize("runtime")
let offset = allocate(size)
// 这里,对于 eWASM 变为一个内存到内存的拷贝,对于 EVM 则相当于 codecopy
datacopy(dataoffset("runtime"), offset, size)
// 这是一个构造函数,并且运行时代码会被返回
return(offset, size)
}
data "Table2" hex"4123"
object "runtime" {
code {
// 运行时代码
let size = datasize("Contract2")
let offset = allocate(size)
// 这里,对于 eWASM 变为一个内存到内存的拷贝,对于 EVM 则相当于 codecopy
datacopy(dataoffset("Contract2"), offset, size)
// 构造函数参数是一个数字 0x1234
mstore(add(offset, size), 0x1234)
create(offset, add(size, 32))
}
// 内嵌对象。使用场景是,外层是一个工厂合约,而 Contract2 将是由工厂生成的代码
object "Contract2" {
code {
// 代码在这 ...
}
object "runtime" {
code {
// 代码在这 ...
}
}
data "Table1" hex"4123"
}
}
}