Yul

Yul(以前はJULIAやIULIAとも呼ばれていました)は、さまざまなバックエンド用のバイトコードにコンパイルできる中間言語です。

スタンドアローンでも、Solidityで「インラインアセンブリ」としても使えます。 コンパイラは、IRベースのコードジェネレータ(「new codegen」または「IR-based codegen」)において、中間言語としてYulを使用します。 Yulは、すべてのターゲットプラットフォームに等しく恩恵を与えることができるハイレベルな最適化段階のための良いターゲットです。

モチベーションとハイレベルな記述

Yulの設計は、次の目標を達成しようと試みています。

  1. Yulで書かれたプログラムは、たとえそれがSolidityや他の高級言語のコンパイラで生成されたコードであっても、可読性がなければいけません。

  2. 手動での検査、形式的な検証、最適化に役立つように、コントロールフローは理解しやすいものでなければなりません。

  3. Yulからバイトコードへの変換は、可能な限り簡単に行う必要があります。

  4. Yulは、プログラム全体の最適化に適しているべきです。

1つ目と2つ目の目標を達成するために、Yulは for ループ、 if 文、 switch 文、関数呼び出しといったハイレベルな要素を提供しています。 アセンブリプログラムのコントロールフローを適切に表現するためには、これらで十分なはずです。 したがって、 SWAPDUPJUMPDESTJUMPJUMPI の明示的な文は用意されていません。 なぜなら、最初の2つはデータフローを難読化し、最後の2つはコントロールフローを難読化するからです。 さらに、 mul(add(x, y), 7) 形式の関数文は 7 y x add mul のような純粋なオペコード文よりも好まれます。 なぜなら、前の形式ではどのオペランドがどのオペコードに使用されているかがわかりやすいからです。

スタックマシン用に設計されているにもかかわらず、Yulはスタック自体の複雑さが現れることがありません。 プログラマーや監査人は、スタックのことを気にする必要はありません。

3つ目の目標は、ハイレベルな構造体を非常に規則的な方法でバイトコードにコンパイルすることで達成されます。 アセンブラが行う唯一の非ローカルな操作は、ユーザー定義の識別子(関数、変数、...)の名前のルックアップと、スタックからのローカル変数のクリーンアップです。

値なのか参照なのかというような混乱を避けるために、Yulは静的に型付けされています。 また、デフォルトの型(通常はターゲットマシンの整数ワード)があり、可読性のために常に省略できます。

言語をシンプルかつ柔軟に保つために、Yulは素の状態ではビルトインの演算や関数、型を持っていません。 これらはYulの方言を指定して初めて、そのセマンティクスとともに追加されます。 これにより、さまざまなターゲットプラットフォームや機能の要件に合わせて、Yulを特殊化できます。

現在、Yulの方言は1つだけあります。 この方言では、EVMのオペコードをビルトイン関数として使用し(下記参照)、EVMのネイティブな256ビット型である u256 型のみを定義しています。 そのため、以下の例では型を記述していません。

シンプルな例

以下のサンプルプログラムはEVM方言で書かれており、指数計算を行います。 solc --strict-assembly を使ってコンパイルできます。 ビルトイン関数 muldiv は、それぞれ積と除算を計算します。

{
    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)ab より小さいかどうかを計算します。

{
    function power(base, exponent) -> result
    {
        result := 1
        for { let i := 0 } lt(i, exponent) { i := add(i, 1) }
        {
            result := mul(result, base)
        }
    }
}

セクションの最後 では、ERC-20規格の完全な実装が見られます。

スタンドアローンでの使用

Yulは、Solidityコンパイラを使用して、EVM方言をスタンドアローンの形で使用できます。 これは 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オブジェクト を参照してください。 このセクションでは、そのようなオブジェクトのコード部分についてのみ説明します。 このコード部分は、常に中括弧で区切られたブロックで構成されています。 ほとんどのツールは、オブジェクトが期待される場所にコードブロックだけを指定することをサポートしています。

コードブロック内では、以下のような要素が使用できます(詳細は後述します)。

  • リテラル(最大32文字までの文字列)。例: 0x12342"abc"

  • ビルトイン関数の呼び出し。例: add(1, mload(0))

  • 変数宣言(初期値として0が代入される)。例: let x := 7let x := add(y, 3)let x

  • 識別子(変数)。例: 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ループ。例: for { let i := 0} lt(i, 10) { i := add(i, 1) } { mstore(i, 7) }

  • 関数定義。例: function f(a, b) -> c { c := add(a, b) }

複数の構文要素は、空白で区切られているだけで、互いに続けることができます。 つまり、終端の ; や改行は必要ありません。

リテラル

リテラルとして次のものを使用できます。

  • 10進数または16進数表記の整数定数。

  • ASCII文字列(例: "abc" )。 N が16進数である場合、16進数エスケープ \xNN とUnicodeエスケープ \uNNNN を含むことができます。

  • 16進数の文字列(例: hex"616263" )。

YulのEVM方言では、リテラルは次のように256ビットのワードを表します。

  • 10進数または16進数の定数は、 2**256 より小さい値でなければなりません。 これらの定数は、256ビットのワードのビッグエンディアンエンコーディングの符号なし整数として表します。

  • ASCII文字列は、まずバイト列として見ることができます。 すなわち、エスケープされていないASCII文字はASCIIコードを値とする1バイトと見なし、エスケープ \xNN はその値を持つ1バイトと見なし、エスケープ \uNNNN はそのコードポイントに対するUTF-8のバイト列と見なします。 バイト列は32バイトを超えてはなりません。 バイト列は32バイトになるように右に0をパディングします。 すなわち、文字列は左詰めで格納します。 パディングされたバイト列は、最上位8ビットが最初のバイトとなる256ビットのワードを表します。 すなわちビッグエンディアン形式で解釈されます。

  • 16進文字列は、まず隣り合う16進文字のペアを1バイトと見なして、バイト列として扱います。 バイト列は32バイト(つまり64個の16進文字)を超えてはならず、上記のように扱われます。

EVM用にコンパイルした場合、これは適切な PUSHi 命令に変換されます。 次の例では、 32 を足して5とし、文字列 "abc" のビット単位の and を計算しています。 最終的な値は、 x というローカル変数に代入されます。

上記の32バイトの制限は、リテラル引数を必要とするビルトイン関数に渡される文字列リテラルに対しては適用されません(例: setimmutableloadimmutable )。 それらの文字列は、生成されるバイトコードには含まれません。

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` は2つの値を返す。
let x, y := f(1, mload(0))

EVMのビルトイン関数では、関数式はオペコードのストリームに直接変換されます。 式を右から左に読むだけでオペコードが得られます。 例の2行目の場合、 PUSH1 3 PUSH1 0x80 MLOAD ADD PUSH1 0x80 MSTORE です。

ユーザー定義関数の呼び出しでは、引数も右から左にスタックに置かれ、これが引数リストが評価される順序となります。 一方、戻り値は左から右へとスタックに置かれます。 つまり、この例では、 y がスタックの一番上に、 x がその下に置かれます。

変数宣言

let キーワードを使って変数を宣言できます。 変数は定義された {...} ブロックの中でのみ使えます。 EVMへのコンパイル時には、変数のために予約された新しいスタックのスロットが作成され、ブロックの終わりに達すると自動的に削除されます。 変数には初期値を指定できます。 値を指定しない場合は、変数はゼロに初期化されます。

変数はスタック上に格納されるため、メモリやストレージに直接影響を与えることはありませんが、ビルトイン関数 mstoremloadsstoresload でメモリやストレージの位置へのポインタとして変数を使用できます。 将来の方言では、このようなポインターのための特定の型が導入されるかもしれません。

変数を参照すると、その変数の現在の値がコピーされます。 EVMでは、これは DUP 命令に相当します。

{
    let zero := 0
    let v := calldataload(zero)
    {
        let y := add(sload(v), 1)
        v := y
    } // yが「deallocated」されます。
    sstore(v, zero)
} // vとzeroが「deallocated」されます。

宣言した変数の型がデフォルトの型と異なる場合は、コロンの後にその型を記述します。 また、複数の値を返す関数呼び出しから代入する場合、1つの文で複数の変数を宣言できます。

// これはコンパイルできません(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

if文は、条件付きでコードを実行するために使用できます。 elseブロックは定義できません。 複数の選択肢が必要な場合は、代わりにswitch(後述)の使用を検討してください。

if lt(calldatasize(), 4) { revert(0, 0) }

カーリーブレースは必須です。

switch

switch文は、if文の拡張版として使うことができます。 switch文は、式の値を受け取り、それをいくつかのリテラル定数と比較します。 そして、一致した定数に対応する分岐が実行されます。 他のプログラミング言語とは異なり、安全上の理由から、制御の流れは1つのケースから次のケースへとは続きません。 default と呼ばれるフォールバックまたはデフォルトケースがあり、リテラル定数のどれにもマッチしない場合に実行されます。

{
    let x := 0
    switch calldataload(4)
    case 0 {
        x := calldataload(0x24)
    }
    default {
        x := calldataload(0x44)
    }
    sstore(0, div(x, 2))
}

ケースのリスト自体は中括弧で囲まれていませんが、ケースの本文では中括弧が必要です。

ループ

Yulは、初期化パート・条件・イテレーション後のパートを含むヘッダーとボディから成るforループをサポートしています。 条件は式でなければならず、他の3つはブロックです。 初期化パートでトップレベルの変数が宣言されている場合、その変数のスコープはループの他のすべての部分にまで及びます。

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の関数と混同してはいけません。 Yulの関数はコントラクトの外部インターフェースの一部ではなく、Solidityの関数とは別の名前空間に属しているからです。

EVMでは、Yulの関数はスタックから引数(およびリターンされるPC)を取り、結果をスタックに置きます。 ユーザー定義関数やビルトイン関数も全く同じように呼び出されます。

関数はどこでも定義でき、宣言されたブロック内で利用できます。 関数の内部では、その関数の外部で定義されたローカル変数にアクセスできません。

関数はSolidityと同様に、パラメータとリターン変数を宣言します。 値を返すには、その値を戻り値の変数に代入します。

複数の値を返す関数を呼び出した場合は、 a, b := f(x)let a, b := f(x) を使って複数の変数に代入する必要があります。

leave 文は、現在の関数を終了するために使用できます。 他の言語の return 文と同じように動作しますが、戻り値を取らずに関数を終了し、関数は戻り値の変数に現在割り当てられている値を返します。

EVM方言には return というビルトイン関数があり、現在のYulの関数だけでなく、完全な実行コンテキスト(内部メッセージコール)を終了させることができることに注意してください。

次の例では、バイナリ法(square-and-multiply)で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オブジェクトの中に配置されます。 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]+

文法に関する制限

文法によって直接課せられるものとは別に、以下のような制限があります。

スイッチには、少なくとも1つのケース(デフォルトのケースを含む)が必要です。 すべてのケースの値は、同じ型で異なる値を持つ必要があります。 式の型のすべての可能な値がカバーされている場合、デフォルトケースは使用できません(つまり、trueとfalseの両方のケースを持つ bool 式のスイッチにはデフォルトケースを使えません)。

すべての式は0個以上の値で評価されます。 識別子とリテラルは正確に1つの値に評価され、関数呼び出しは呼び出された関数のリターン変数の数に等しい数の値に評価されます。

変数宣言や代入では、右辺の式(存在する場合)は、左辺の変数の数と同じ数の値に評価されなければなりません。 これが複数の値に評価される式が許される唯一のシチュエーションです。 代入や変数宣言の左辺には、同じ変数名を複数回使用できません。

文でもある式(ブロックレベル)は、0に評価されなければなりません。

それ以外のシチュエーションでは、式は正確に1つの値に評価されなければなりません。

continue 文または break 文は、以下のようにforループのボディ内でのみ使用できます。 文を含む最も内側のループを考えてみましょう。 ループと文は同じ関数内にあるか、または両方がトップレベルになければなりません。 文はループのボディブロック内に配置しなければならず、ループの初期化ブロックや更新ブロック内に配置できません。 この制限は、 continue 文または break 文を含む最も内側のループにのみ適用されることを強調しておきます。 この最も内側のループ、つまり continue 文または break 文は、外側ループのどこでも、おそらく外側ループの初期化ブロックや更新ブロックでも出現できます。 例えば、次の例は、 break が外側ループの更新ブロックにあるにもかかわらず、内側ループのボディブロックにあるため、合法です:

for {} true { for {} true {} { break } }
{
}

forループの条件部分は、正確に1つの値に評価されなければなりません。

leave 文は、関数内でのみ使用できます。

関数はforループの初期化ブロック内のどこにも定義できません。

リテラルはその型より大きくできません。 定義されている最大の型は256ビット幅です。

代入や関数呼び出しの際には、それぞれの値の型が一致していなければなりません。 暗黙の型変換はありません。 一般に、型の変換は、ある型の値を受け取り、異なる型の値を返す適切なビルトイン関数を方言が提供している場合にのみ実現します。

スコープのルール

Yulでは、スコープはブロックに紐付けられており(例外として、後述する関数やforループがあります)、すべての宣言( FunctionDefinitionVariableDeclaration )は、これらのスコープに新しい識別子を導入します。

識別子は、定義されているブロック(すべてのサブノードとサブブロックを含む)で使用できます。 関数はブロック全体(定義前も含む)で使用できますが、変数は VariableDeclaration の後の文からしか使用できません。

特に、変数は自分の変数宣言の右側では参照できません。 関数は、その宣言の前にすでに参照できます(関数が利用できる場合)。

一般的なスコープルールの例外として、forループの初期化パート(最初のブロック)のスコープは、forループの他のすべてのパートに及びます。 つまり、初期化パート(初期化パート内のブロックは含まない)で宣言された変数(および関数)は、forループの他のすべてのパートで使用できます。

forループの他の部分で宣言された識別子は、通常の構文上のスコープルールに従います。

これは、 for { I... } C { P... } { B... } という形式のforループが { I... for {} C { P... } { B... } } と同等であることを意味しています。

関数のパラメータとリターンパラメータは、関数ボディで利用でき、それらの名前は異なるものでなければなりません。

関数内では、その関数の外で宣言された変数を参照できません。

シャドーイングは禁止されています。 つまり、現在の関数の外で宣言されたために参照できなくても、同じ名前の別の識別子が見える場所で識別子を宣言できません。

形式的な仕様

ASTの様々なノード上でオーバーロードされた評価関数Eを提供することで、Yulを形式的に定めます。 ビルトイン関数には副作用があるため、Eは2つの状態オブジェクトとASTノードを受け取り、2つの新しい状態オブジェクトと可変数の他の値を返します。 2つの状態オブジェクトとは、グローバル状態オブジェクト(EVMの文脈では、ブロックチェーンのメモリ、ストレージ、状態)と、ローカル状態オブジェクト(ローカル変数の状態、つまりEVMのスタックのセグメント)です。

ASTノードが文の場合、Eは2つの状態オブジェクトと breakcontinueleave 文で使用される「モード」を返します。 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 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の方言です。 この方言で使用できる型は、Ethereum Virtual Machineの256ビットのネイティブ型である u256 のみです。 これはこの方言のデフォルト型なので、省略できます。

次の表は、すべてのビルトイン関数(これはEVMバージョンに依存します)をリストアップし、関数/オペコードのセマンティクスの簡単な説明を提供しています。 このドキュメントは、Ethereum Virtual Machineの完全な説明を目的としていません。 正確なセマンティクスに興味がある場合は、別のドキュメントを参照してください。

- と書かれたオペコードは結果を返さず、その他のオペコードは正確に1つの値を返します。 FHBCILP と書かれたオペコードは、それぞれFrontier、Homestead、Byzantium、Constantinople、Istanbul、London、Parisから存在しています。

以下では、 mem[a...b) は位置 a から位置 b までのメモリのバイトを意味し、 storage[p] はスロット p のストレージ内容を意味します。

Yulはローカル変数やコントロールフローを管理しているため、これらの機能を阻害するオペコードは使用できません。 これには、 dupswap 命令のほか、 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。ただしy == 0ならば0。

sdiv(x, y)

F

x / y(2の補数の符号付き数値用)。ただしy == 0ならば0。

mod(x, y)

F

x % y。ただしy == 0ならば 0。

smod(x, y)

F

x % y(2の補数の符号付き数値用)。ただしy == 0ならば0。

exp(x, y)

F

xのy乗。

not(x)

F

xのビット単位のnot(xの各ビットが否定される)。

lt(x, y)

F

x < yの場合は1、それ以外の場合は0。

gt(x, y)

F

x > yの場合は1、それ以外の場合は0。

slt(x, y)

F

2の補数の符号付き数値で、x < yの場合は1、それ以外の場合は0。

sgt(x, y)

F

2の補数の符号付き数値で、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のビット単位のand。

or(x, y)

F

xとyのビット単位のor。

xor(x, y)

F

xとyのビット単位のxor。

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

xの(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。

msize()

F

メモリのサイズ、すなわちアクセスされた最大のメモリインデックス。

gas()

F

残りの実行可能なガス。

address()

F

現在のコントラクト/実行コンテキストのアドレス。

balance(a)

F

アドレスaのwei残高。

selfbalance()

I

balance(address())と同等だが、より安価。

caller()

F

コール送信者( delegatecall を除く)。

callvalue()

F

現在のコールと一緒に送信されたwei。

calldataload(p)

F

位置pから始まるコールデータ(32バイト)。

calldatasize()

F

コールデータのサイズ(バイト)。

calldatacopy(t, f, s)

-

F

位置fのコールデータから位置tのmemにsバイトコピー。

codesize()

F

現在のコントラクト/実行コンテキストのコードサイズ。

codecopy(t, f, s)

-

F

位置fのコードから位置tのmemにsバイトコピー。

extcodesize(a)

F

アドレスaのコードのサイズ。

extcodecopy(a, t, f, s)

-

F

codecopy(t, f, s)と似ているが、アドレスaのコードに対してのもの。

returndatasize()

B

直前のリターンデータのサイズ。

returndatacopy(t, f, s)

-

B

位置fのリターンデータから位置tのmemにsバイトコピー。

extcodehash(a)

C

アドレスaのコードハッシュ。

create(v, p, n)

F

mem[p...(p+n))のコードを持つ新しいコントラクトを作成し、 v Weiを送信し、新しいアドレスを返す。エラーの場合は0を返します。

create2(v, p, n, s)

C

mem[p...(p+n))のコードを持つ新しいコントラクトをアドレス keccak256(0xff . this . s . keccak256(mem[p...(p+n)))に作成し、 v Weiを送信し、新しいアドレスを返します。 0xff は1バイトの値、 this は現在のコントラクトのアドレス(20バイトの値)、 s はビッグエンディアンの256ビットの値です。 エラーの場合は0を返します。

call(g, a, v, in, insize, out, outsize)

F

アドレスaのコントラクトを、 mem[in...(in+insize))を入力として呼び出し、 g gasとv Weiを送信します。 出力をmem[out...(out+outsize))に書き込みます。 エラーの場合(例えば、out of gas)は0を、成功した場合は1を返します。 詳細

callcode(g, a, v, in, insize, out, outsize)

F

call と似ていますが、aのコードのみを使用し、 それ以外は現在のコントラクトのコンテキスト。 詳細

delegatecall(g, a, in, insize, out, outsize)

H

calldata と似ていますが、 callercallvalue も保持します。 詳細

staticcall(g, a, in, insize, out, outsize)

B

call(g, a, 0, in, insize, out, outsize) と似ていますが、 ステートの変更を許可しません。 詳細

return(p, s)

-

F

実行終了。リターンデータはmem[p...(p+s))。

revert(p, s)

-

B

実行終了。ステート変化をリバートします。リターンデータはmem[p...(p+s))。

selfdestruct(a)

-

F

実行終了。現在のコントラクトを破壊し、aに資金を送ります。(非推奨)

invalid()

-

F

invalid命令で実行を終了。

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)。

origin()

F

トランザクションの送信者。

gasprice()

F

トランザクションのガスプライス。

blockhash(b)

F

ブロック番号bのハッシュ - 現在を除く過去256ブロック分のみ。

coinbase()

F

現在のマイニングの受益者アドレス。

timestamp()

F

現在のブロックのタイムスタンプ(エポックからの秒数)。

number()

F

現在のブロックナンバー。

difficulty()

F

現在のブロックの難易度。(下記の注釈も参照)

prevrandao()

P

ビーコンチェーンによるランダム性(下記の注釈も参照)

gaslimit()

F

現在のブロックのブロックガスリミット。

注釈

call* 命令は、 out および outsize のパラメータを使用して、戻り値または失敗の値のデータを配置するメモリ内の領域を定義します。 この領域は、呼び出されたコントラクトが何バイト返すかに応じて、書き込まれます。 より多くのデータを返した場合は、最初の outsize バイトのみが書き込まれます。 残りのデータには returndatacopy オペコードでアクセスできます。 より少ないデータを返した場合は、残りのバイトにはまったく手をつけません。 このメモリ領域のどの部分にリターンデータが含まれているかを確認するには、 returndatasize オペコードを使用する必要があります。 残りのバイトは、呼び出し前の値を保持します。

注釈

Paris以降のEVMのバージョンでは、 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オブジェクトの他の部分にアクセスするために使用されます。

datasizedataoffset は、文字列リテラル(他のオブジェクトの名前)のみを引数に取り、それぞれデータ領域のサイズとオフセットを返します。 EVMでは、 datacopy 関数は codecopy と同等です。

setimmutable, loadimmutable

関数 setimmutable(offset, "name", value)loadimmutable("name") はSolidityのimmutable機構に使用されており、純粋なYulにはうまくマッピングされていません。 setimmutable(offset, "name", value) の呼び出しは、指定された名前付きのimmutableコントラクトを含むランタイムコードがオフセット offset でメモリにコピーされたと仮定し、ランタイムコード内の loadimmutable("name") への呼び出しのために生成されたプレースホルダーを含むメモリ内のすべての位置( offset に対する相対位置)に value を書き込みます。

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 は複数回呼び出すことができますが、1つのYulサブオブジェクト内で同じリテラルを引数として持つ必要があります。 サブオブジェクトの中に少なくとも1つの memoryguard の呼び出しが見つかった場合、追加のオプティマイザのステップが実行されます。

verbatim

verbatim... ビルトイン関数群は、Yulコンパイラーが知らないオペコードのバイトコードを作成できます。 また、オプティマイザによって変更されることがないバイトコードシーケンスを作成することもできます。

関数は verbatim_<n>i_<m>o("<data>", ...) のようになります。 ここで、

  • n は0~99の10進数で、入力スタックのスロット/変数の数を指定します。

  • m は0~99の10進数で、出力スタックのスロット/変数の数を指定します。

  • data はバイト列を含む文字列リテラルです。

例えば、入力を2倍する関数を定義する際に、オプティマイザが定数2に触れないようにするには、次のようにします。

let x := calldataload(0)
let double := verbatim_1i_1o(hex"600202", x)

このコードでは、 x を取得するための dup1 オペコード(オプティマイザは calldataload オペコードの結果を直接再利用するかもしれませんが)が、 600202 に続いて表示されます。 このコードは、 x のコピーされた値を消費して、スタックの一番上に結果を生成すると想定されます。 その後、コンパイラは double 用のスタックスロットを割り当て、そこに結果を格納するコードを生成します。

他のオペコードと同様に、引数はスタック上に左端の引数が一番上になるように並べられ、戻り値は右端の変数がスタックの一番上になるように並べられるとされています。

verbatim は、任意のオペコードや、Solidityコンパイラにとって未知のオペコードを生成するために使用できるため、オプティマイザと verbatim を併用する際には注意が必要です。 オプティマイザがオフになっていても、コードジェネレーターはスタックレイアウトを決定しなければなりません。 つまり、 verbatim を使ってスタックの高さを変更すると、未定義の動作になる可能性があります。

以下は、コンパイラではチェックされない逐語的バイトコードの制限事項の非網羅的なリストです。 これらの制限に違反すると、未定義の動作を引き起こす可能性があります。

  • コントロールフローはverbatimブロックの中に飛び込んだり、外に出たりしてはいけませんが、同じverbatimブロックの中では飛び込むことができます。

  • 入力と出力パラメータ以外のスタックの内容にアクセスしてはいけません。

  • スタックの高さの差は、正確には m - n (出力スロットから入力スロットを引いたもの)です。

  • Verbatimのバイトコードは、周囲のバイトコードを想定できません。必要なパラメータはすべてスタック変数として渡さなければなりません。

オプティマイザはバイトコードを逐語的に分析せず、常に状態のすべての側面を修正することを前提としているため、 verbatim 関数呼び出し全体ではごくわずかな最適化しかできません。

オプティマイザは、バーベイタムバイトコードを不透明なコードブロックとして扱います。 分割はしませんが、移動、複製、同一のバーベイタムバイトコードブロックとの結合は可能です。 逐語的バイトコードブロックがコントロールフローから到達できない場合、そのブロックは削除されます。

警告

EVMの改善が既存のスマートコントラクトを破壊するかどうかを議論する際、 verbatim の機能はSolidityのコンパイラ自体が使用する機能と同じように考慮できません。

注釈

混乱を避けるため、文字列 verbatim で始まる識別子はすべて予約されており、ユーザー定義の識別子には使用できません。

Yulオブジェクトの仕様

Yulオブジェクトは、名前の付いたコードおよびデータセクションをグループ化するために使用されます。 関数 datasizedataoffsetdatacopy を使用して、コード内からこれらのセクションにアクセスできます。 16進文字列は、データを16進エンコーディングで、通常の文字列をネイティブエンコーディングで指定するために使用できます。 コードの場合、 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オプティマイザによってデプロイされたコードとして扱われます。 この唯一の結果は、オプティマイザのガスコストのヒューリスティックが異なるということです。

注釈

. を含む名前のデータオブジェクトやサブオブジェクトを定義できますが、 . は他のオブジェクトの内部にあるオブジェクトにアクセスするためのセパレータとして使用されるため、 datasizedataoffsetdatacopy を介してアクセスできません。

注釈

".metadata" というデータオブジェクトには特別な意味があります: コードからはアクセスできず、オブジェクト内の位置に関わらず、常にバイトコードの最後尾に付加されます。

今後、特別な意味を持つデータオブジェクトが他に追加されるかもしれませんが、その名前は常に . で始まります。

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

// コントラクトは単一のオブジェクトで構成され、デプロイされるコードや作成できる他のコントラクトを表すサブオブジェクトがあります。
// 1つのcodeノードは、オブジェクトの実行可能なコードです。
// すべての(他の)名前の付いたオブジェクトやデータセクションはシリアライズされ、特別な組み込み関数datacopy / dataoffset / datasizeでアクセスできるようになります。
// カレントオブジェクト、サブオブジェクト、カレントオブジェクト内部のデータアイテムがスコープに入ります。
object "Contract1" {
    // コントラクトのコンストラクタコード
    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))
        }

        // 最初に"Contract2"を作成する
        let size := datasize("Contract2")
        let offset := allocate(size)
        // これは、EVMのコードコピーになる
        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ではコードコピーになる
        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))
            }

            // ランタイムコード

            mstore(0, "Hello, World!")
            return(0, 0x20)
        }
    }

    // 埋め込みオブジェクト。使用例としては、外側がファクトリーのコントラクトで、Contract2がファクトリーで作成されるコード。
    object "Contract2" {
        code {
            // code here ...
        }

        object "Contract2_deployed" {
            code {
                // code here ...
            }
        }

        data "Table1" hex"4123"
    }
}

Yulオプティマイザ

Yulオプティマイザは、Yulコード上で動作し、入力、出力、中間状態を同じ言語で表現します。 これにより、オプティマイザのデバッグや検証が容易になります。

各最適化ステージの詳細やオプティマイザの使用方法については、一般的な オプティマイザドキュメント を参照してください。

SolidityをスタンドアローンのYulモードで使いたい場合は、 --optimize でオプティマイザを起動し、オプションで --optimize-runs期待コントラクト実行回数 を指定します。

solc --strict-assembly --optimize --optimize-runs 200

Solidityモードでは、通常のオプティマイザと一緒にYulオプティマイザが実行されます。

最適化ステップシーケンス

最適化の順序や略語のリストに関する詳しい情報は オプティマイザのドキュメント にあります。

ERC20の完全な例

object "Token" {
    code {
        // スロット0にコントラクト作成者を格納
        sstore(0, caller())

        // コントラクトのデプロイ
        datacopy(0, dataoffset("runtime"), datasize("runtime"))
        return(0, datasize("runtime"))
    }
    object "runtime" {
        code {
            // Ether送信の保護
            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)
            }

            /* ---------- コールデータのデコード関数 ----------- */
            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)
            }
            /* ---------- コールデータのエンコード関数 ---------- */
            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) }
            }
        }
    }
}