Yul

Yul(以前JULIAやIULIAと呼ばれていたものです)は複数の異なるバックエンド(EVM1.0やEVM1.5、eWASM)でコンパイルできる中間言語です。 そのため、Yulは3つのプラットフォームすべての使用可能な共通分母になるように設計されています。 YulはすでにSolidity内の"inline assembly"としても使用されており、Solidityコンパイラの将来のバージョンではYulを中間言語として使用できるようになります。 また、Yul用の高水準オプティマイザステージを構築するのは簡単です。

注釈

"inline assembly"として使用されるときに型を持つわけではありません(すべて u256 になります)。 そして、組み込み関数はEVMオペコードと同じです。詳細についてはinline assemblyのドキュメントを参照してください。

Yulのコアコンポーネントは関数、ブロック、変数、リテラル、forループ、if文、switch文、式と変数への代入です。

Yulは型付けされ、変数とリテラルの両方が後置記法で型を指定しなければなりません。 サポートしている型は、 boolu8s8u32s32u64s64u128s128u256s256 です。

Yul自体は、演算子すら持ちません。もしあるEVMが指定された場合、オペコードは組み込み関数として利用可能になりますが、バックエンドが変更された場合は再実装することができます。 マンダトリーなビルドイン関数のリストは下のセクションを参照ください。

次のプログラムの例では、EVMのオペコード muldiv、および mod がネイティブでも関数としても利用可能であると仮定して、べき乗を計算します。

{
    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ループを使用して同じ関数を実装することも可能です。 ここで、利用可能なEVMオペコードの lt (less-than)と 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)
        }
    }
}

Specification of Yul

この章では、Yulのコードを記述します。通常、Yulオブジェクトの中に置かれるものです。これについては次の章で説明します。

Grammar:

Block = '{' Statement* '}'
Statement =
    Block |
    FunctionDefinition |
    VariableDeclaration |
    Assignment |
    If |
    Expression |
    Switch |
    ForLoop |
    BreakContinue
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'
FunctionCall =
    Identifier '(' ( Expression ( ',' Expression )* )? ')'
Identifier = [a-zA-Z_$] [a-zA-Z_$0-9]*
IdentifierList = Identifier ( ',' Identifier)*
TypeName = Identifier | BuiltinTypeName
BuiltinTypeName = 'bool' | [us] ( '8' | '32' | '64' | '128' | '256' )
TypedIdentifierList = Identifier ':' TypeName ( ',' Identifier ':' TypeName )*
Literal =
    (NumberLiteral | StringLiteral | HexLiteral | TrueLiteral | FalseLiteral) ':' TypeName
NumberLiteral = HexNumber | DecimalNumber
HexLiteral = 'hex' ('"' ([0-9a-fA-F]{2})* '"' | '\'' ([0-9a-fA-F]{2})* '\'')
StringLiteral = '"' ([^"\r\n\\] | '\\' .)* '"'
TrueLiteral = 'true'
FalseLiteral = 'false'
HexNumber = '0x' [0-9a-fA-F]+
DecimalNumber = [0-9]+

Restrictions on the Grammar

Switch文はdefault caseを含む最低でも1つのcaseを持ちます。 もし式のすべての可能な値がカバーされている場合、defaultのcaseは許容するべきではありません(すなわち、真と偽の両方のケースを持つ bool を持つSwitch文)。 また、すべてのcaseの値は同じ型である必要があります。

すべての式はゼロ以上の値に評価されます。識別子とリテラルは厳密に1つの値であると評価され、関数呼び出しは呼び出された関数の戻り値の数と等しい数の値であると評価されます。

変数宣言時や代入時、右辺の式は、左辺の変数の数と等しい数の値であると評価される必要があります。 これが、複数の値に評価される式が許可される唯一の状況です。

ステートメント(すなわち、ブロックレベル)でもある式は、0として評価される必要があります。 そして、その他のすべてのシチュエーションにおいて、式は単一の値として評価する必要があります。

continuebreak 文はループ文中でのみ使用でき、ループ文と同じ関数内になければなりません(もしくは両方ともトップレベルになければなりません)。

for文の条件は、単一の値で評価される必要があります。

リテラルは、その型以上に大きくなることはありません。最大値の型は、256ビット長であることが決められています。

Scoping Rules

Yulのスコープはブロックと結びついており(関数とforループは例外です)、すべての宣言( FunctionDefinitionVariableDeclaration)はこれらのスコープに新たな識別子をもたらします。

識別子は、定義されたブロック内で表示することができます(すべてのサブノートやサブブロックを含みます)。 例外として、forループの"init"部分(最初のブロック)で定義された識別子は、forループの他のすべての部分で表示することができます(ただし、ループの外側では表示できません)。 関数のパラメータと戻り値は関数本体に表示され、それらの名前は同じものを使用することはできません。

変数は宣言後に参照することができます。特に、変数はそれ自身の変数宣言の右側では参照できません。 関数は宣言前にすでに参照できます(可視性である場合に限ります)。

シャドーイングは使用できません。つまり、たとえアクセスできない場合でも、同じ名前の別の識別子も表示されている場所で識別子を宣言することはできません。

また、関数の外側で宣言された変数にアクセスすることはできません。

Formal Specification

私たちは、ASTのさまざまなノードにオーバーロードされた評価関数Eを提供して、Yulを形式的に指定します。 どの関数にも副作用がある可能性があるため、評価関数Eは2つのステートオブジェクトとASTノードを取り、2つの新しいステートオブジェクトと可変数の他の値を返します。 2つの状態オブジェクトとはグローバルステートオブジェクト(EVMのコンテキストではブロックチェーンのメモリ、ストレージ、およびステート)とローカルステートオブジェクト(ローカル変数のステート、つまりEVM内のスタックのセグメント)です。

もしASTノードがステートメントである場合、評価関数Eは2つのステートオブジェクトと、 breakcontinue に使用される "mode"を返します。 もしASTノードが式である場合、評価関数Eは2つのステートオブジェクトと式が評価する同じ数の値を返します。

グローバルステートの正確な性質は、この上位レベルの説明では規定されていません。 ローカルステート 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

Type Conversion Functions

Yulは暗黙的型変換をサポートしていないため、明示的変換を提供するための関数が存在します。 大きな型からより小さな型へ変換するとき、オーバーフローの場合にruntime exceptionが発生する可能性があります。

以下の型間での変換の切り捨てがサポートされています:
  • bool
  • u32
  • u64
  • u256
  • s256

これらのそれぞれに対して、型変換関数は、 u32tobool(x:u32) -> y:boolu256tou32(x:u256) -> y:u32s256tou256(x:s256) -> y:u256 などといった <input_type>to<output_type>(x:<input_type>) -> y:<output_type> 形式のプロトタイプを持ちます。

注釈

u32tobool(x:u32) -> y:booly := not(iszerou256(x)) として実行され、 booltou32(x:bool) -> y:u32switch x case true:bool { y := 1:u32 } case false:bool { y := 0:u32 } として実行されます。

Low-level Functions

以下の関数が利用可能でなければなりません:

Logic
not(x:bool) -> z:bool logical not
and(x:bool, y:bool) -> z:bool logical and
or(x:bool, y:bool) -> z:bool logical or
xor(x:bool, y:bool) -> z:bool xor
Arithmetic
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, for signed numbers in two's complement
modu256(x:u256, y:u256) -> z:u256 x % y
mods256(x:s256, y:s256) -> z:s256 x % y, for signed numbers in two's complement
signextendu256(i:u256, x:u256) -> z:u256 sign extend from (i*8+7)th bit counting from least significant
expu256(x:u256, y:u256) -> z:u256 x to the power of y
addmodu256(x:u256, y:u256, m:u256) -> z:u256 (x + y) % m with arbitrary precision arithmetic
mulmodu256(x:u256, y:u256, m:u256) -> z:u256 (x * y) % m with arbitrary precision arithmetic
ltu256(x:u256, y:u256) -> z:bool true if x < y, false otherwise
gtu256(x:u256, y:u256) -> z:bool true if x > y, false otherwise
lts256(x:s256, y:s256) -> z:bool true if x < y, false otherwise (for signed numbers in two's complement)
gts256(x:s256, y:s256) -> z:bool true if x > y, false otherwise (for signed numbers in two's complement)
equ256(x:u256, y:u256) -> z:bool true if x == y, false otherwise
iszerou256(x:u256) -> z:bool true if x == 0, false otherwise
notu256(x:u256) -> z:u256 ~x, every bit of x is negated
andu256(x:u256, y:u256) -> z:u256 bitwise and of x and y
oru256(x:u256, y:u256) -> z:u256 bitwise or of x and y
xoru256(x:u256, y:u256) -> z:u256 bitwise xor of x and y
shlu256(x:u256, y:u256) -> z:u256 logical left shift of x by y
shru256(x:u256, y:u256) -> z:u256 logical right shift of x by y
sars256(x:s256, y:u256) -> z:u256 arithmetic right shift of x by y
byte(n:u256, x:u256) -> v:u256 nth byte of x, where the most significant byte is the 0th byte Cannot this be just replaced by and256(shr256(n, x), 0xff) and let it be optimised out by the EVM backend?
Memory and storage
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 - only modifies a single byte
sload(p:u256) -> v:u256 storage[p]
sstore(p:u256, v:u256) storage[p] := v
msize() -> size:u256 size of memory, i.e. largest accessed memory index, albeit due due to the memory extension function, which extends by words, this will always be a multiple of 32 bytes
Execution control
create(v:u256, p:u256, n:u256) create new contract with code mem[p..(p+n)) and send v wei and return the new address
create2(v:u256, p:u256, n:u256, s:u256) create new contract with code mem[p...(p+n)) at address keccak256(0xff . this . s . keccak256(mem[p...(p+n))) and send v wei and return the new address, where 0xff is a 8 byte value, this is the current contract's address as a 20 byte value and s is a big-endian 256-bit value
call(g:u256, a:u256, v:u256, in:u256, insize:u256, out:u256, outsize:u256) -> r:u256 call contract at address a with input mem[in..(in+insize)) providing g gas and v wei and output area mem[out..(out+outsize)) returning 0 on error (eg. out of gas) and 1 on success
callcode(g:u256, a:u256, v:u256, in:u256, insize:u256, out:u256, outsize:u256) -> r:u256 identical to call but only use the code from a and stay in the context of the current contract otherwise
delegatecall(g:u256, a:u256, in:u256, insize:u256, out:u256, outsize:u256) -> r:u256 identical to callcode, but also keep caller and callvalue
abort() abort (equals to invalid instruction on EVM)
return(p:u256, s:u256) end execution, return data mem[p..(p+s))
revert(p:u256, s:u256) end execution, revert state changes, return data mem[p..(p+s))
selfdestruct(a:u256) end execution, destroy current contract and send funds to a
log0(p:u256, s:u256) log without topics and data mem[p..(p+s))
log1(p:u256, s:u256, t1:u256) log with topic t1 and data mem[p..(p+s))
log2(p:u256, s:u256, t1:u256, t2:u256) log with topics t1, t2 and data mem[p..(p+s))
log3(p:u256, s:u256, t1:u256, t2:u256, t3:u256) log with topics t, t2, t3 and data mem[p..(p+s))
log4(p:u256, s:u256, t1:u256, t2:u256, t3:u256, t4:u256) log with topics t1, t2, t3, t4 and data mem[p..(p+s))
State queries
blockcoinbase() -> address:u256 current mining beneficiary
blockdifficulty() -> difficulty:u256 difficulty of the current block
blockgaslimit() -> limit:u256 block gas limit of the current block
blockhash(b:u256) -> hash:u256 hash of block nr b - only for last 256 blocks excluding current
blocknumber() -> block:u256 current block number
blocktimestamp() -> timestamp:u256 timestamp of the current block in seconds since the epoch
txorigin() -> address:u256 transaction sender
txgasprice() -> price:u256 gas price of the transaction
gasleft() -> gas:u256 gas still available to execution
balance(a:u256) -> v:u256 wei balance at address a
this() -> address:u256 address of the current contract / execution context
caller() -> address:u256 call sender (excluding delegatecall)
callvalue() -> v:u256 wei sent together with the current call
calldataload(p:u256) -> v:u256 call data starting from position p (32 bytes)
calldatasize() -> v:u256 size of call data in bytes
calldatacopy(t:u256, f:u256, s:u256) copy s bytes from calldata at position f to mem at position t
codesize() -> size:u256 size of the code of the current contract / execution context
codecopy(t:u256, f:u256, s:u256) copy s bytes from code at position f to mem at position t
extcodesize(a:u256) -> size:u256 size of the code at address a
extcodecopy(a:u256, t:u256, f:u256, s:u256) like codecopy(t, f, s) but take code at address a
extcodehash(a:u256) code hash of address a
Others
discard(unused:bool) discard value
discardu256(unused:u256) discard value
splitu256tou64(x:u256) -> (x1:u64, x2:u64, x3:u64, x4:u64) split u256 to four u64's
combineu64tou256(x1:u64, x2:u64, x3:u64, x4:u64) -> (x:u256) combine four u64's into a single u256
keccak256(p:u256, s:u256) -> v:u256 keccak(mem[p...(p+s)))
Object access  
datasize(name:string) -> size:u256 size of the data object in bytes, name has to be string literal
dataoffset(name:string) -> offset:u256 offset of the data object inside the data area in bytes, name has to be string literal
datacopy(dst:u256, src:u256, len:u256) copy len bytes from the data area starting at offset src bytes to memory at position dst

Backends

バックエンドやターゲットはYulから特定のバイトコードへのtranslatorとなります。各バックエンドは、バックエンドの名前を接頭辞として持つ関数を公開することができます。 また、バックエンドのために evmewasm の接頭辞を予約語としています。

Backend: EVM

EVMターゲットは evm_ 接頭辞で公開されているすべての基底のEVMオペコードを持ちます。

Backend: "EVM 1.5"

TBD

Backend: eWASM

TBD

Specification of Yul Object

Yulオブジェクトは、名前付きコードとデータセクションをグループ化するために使用されます。 関数 datasizedataoffset および datacopy は、コード内からこれらのセクションにアクセスするために使用できます。 また、16進数エンコーディングでデータを指定し、ネイティブエンコーディングで通常の文字列を指定するために16進数の文字列を使用できます。 コードの場合、 datacopy はassembled binary representationにアクセスします。

Grammar:

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 を表します。

Yulオブジェクトの例を以下に示します:

// コードは単一のオブジェクトで構成されています。単一の "code"ノードはオブジェクトのコードです。
// すべての(他の)名前付きオブジェクトまたはデータセクションはシリアライズされ、
// 特別な組み込み関数 datacopy/dataoffset/datasize にアクセスできるようになります。
// ネストされたオブジェクトへのアクセスは"."を使って名前を結合することによって可能です。
// 現在のオブジェクト、現在のオブジェクト内のサブオブジェクト、およびデータ項目は、ネストアクセスなしでスコープ内にあります。
object "Contract1" {
    code {
        // 最初に "runtime.Contract2" を作成します
        let size = datasize("runtime.Contract2")
        let offset = allocate(size)
        // 以下はeWASMの場合はmemory-> memory、
        // EVMの場合は "runtime.Contract2"のコードコピーになります
        datacopy(offset, dataoffset("runtime.Contract2"), size)
        // コンストラクタパラメータは単一値0x1234になります
        mstore(add(offset, size), 0x1234)
        create(offset, add(size, 32))

        // これでランタイムオブジェクトを返すようになりました(これはコンストラクタコードです)
        size := datasize("runtime")
        offset := allocate(size)
        // 以下はeWASMの場合はmemory-> memory、EVMの場合はcodecopyになります。
        datacopy(offset, dataoffset("runtime"), size)
        return(offset, size)
    }

    data "Table2" hex"4123"

    object "runtime" {
        code {
            // ランタイムコード

            let size = datasize("Contract2")
            let offset = allocate(size)
            // 以下はeWASMの場合はmemory-> memory、EVMの場合はcodecopyになります。
            datacopy(offset, dataoffset("Contract2"), size)
            // コンストラクタパラメータは単一値0x1234になります
            mstore(add(offset, size), 0x1234)
            create(offset, add(size, 32))
        }

        // 埋め込みオブジェクト。ユースケースとして、オブジェクト外においてファクトリコントラクトであり、
        // Contract2がファクトリによって作成されるコードです。
        object "Contract2" {
            code {
                // コード...
            }

            object "runtime" {
                code {
                    // コード...
                }
             }

             data "Table1" hex"4123"
        }
    }
}