2.7 中間コード(WebAssembly)生成
構文解析が出力した構文木と、構文木に含まれる名前に関する情報を意味解析で登録したハッシュ表から取得し、中間コードを出力する処理を中間コード生成と呼びます。 本書の中間コードの形式は、WebAssembly バイナリ表現です。この章で作るコンパイラでは、 int_calc_compiler/src/lib/tp_compiler/tp_make_wasm.c :tp_make_wasm 関数が該当します。
この節は、WebAssembly の公式資料を参照しながら、読むようにすると理解が進むと思います。
Binary Format — WebAssembly https://webassembly.github.io/spec/core/binary/index.html
WebAssembly Specifications https://webassembly.github.io/spec/
日本語で読みたい方は、以下の電子書籍を参照してください。
WebAssemblyをはじめよう | WEBASSEMBLY USUI BOOK https://ukyo.github.io/wasm-usui-book/webroot/get-started-webassembly.html
2.7.1 既存のツール wabt を使って、WebAssembly を知る
Microsoft Store から、WSL の Ubuntu をインストールしてください。この節の執筆時には Ubuntu 20.04 LTS を利用しました。WSL の Ubuntu で、以下の手順で wabt をビルドします。 インストールされていないコマンドがあると指摘された場合は、その都度インストールするようにしてください。 同じ版を試したい場合は、git checkout してください。ビルドエラーになった場合に、「apt-file search ファイル名」でパッケージを探すと、問題解決のヒントが得られる可能性があります。 Ubuntu のパッケージが壊れていて作業の続行ができなくなった場合は、Windows の「設定 - アプリ - アプリと機能 - Ubuntu 20.04 LTS - 詳細オプション」のリセット・ボタンを押下してください。
$ sudo apt update
$ sudo apt upgrade -fy
$ sudo apt autoremove
$ sudo apt install -fy apt-file
$ sudo apt-file update
$
$ sudo apt install cmake clang -fy
$ git clone --recursive https://github.com/WebAssembly/wabt
$ cd wabt
$ git show -s --format=%H
7eadc12f71483b1d9d8cf16877efa33361d1e493
$
$ cd build
$ cmake ..
$ cmake --build .例として、以下のようなソースコードをコンパイルできることを前提としています。
上記のソースコードを C 言語で書き直すと、以下のようになります。
WebAssembly は、スタックを使って処理を進めていく「スタックマシン」なので、例として上記の calc 関数の 1 行目の式:(1 + 2) * 3 をスタックマシンで表現してみます。 まず、式:(1 + 2) * 3 を逆ポーランド記法:1 2 + 3 * に書き換えます。演算子が後に来るので、後置記法とも呼ばれます。式の左から順にスタックを使って計算をすると、 以下のようにスタックの状態が変化し、計算結果 9 が得られます。push は、スタックに値を積む操作で、演算子 + と * はスタックに積まれた値を取り出して、計算した結果を スタックに積みます。
C 言語で書いた calc 関数を、スタックマシンをイメージしながら WebAssembly のテキスト表現(Flat 構文)に書き直すと、以下のようになります。calc.wat ファイルとして保存します。 (export "calc" (func $func0)) 以下が、C 言語で書いた calc 関数に対応する内容になっています。set_local は、スタック最上部の値を取り出してローカル変数に代入します。 get_local は、ローカル変数の値をスタックに積みます。tee_local は、スタック最上部の値を取り出してローカル変数に代入し、同じ値をスタックに積みます。
次に、WebAssembly のテキスト表現 calc.wat ファイルを WebAssembly バイナリ表現 calc.wasm に変換し、WebAssembly のテキスト表現 calc2.wat ファイルに変換します。
その結果、以下のように人間にとって読みやすくするための情報が欠落しますが、その分だけ WebAssembly バイナリ表現に近いイメージの WebAssembly テキスト表現が得られます。
WebAssembly バイナリ表現 calc.wasm ファイルの内容をダンプしてみます。これだと、慣れないと分かりにくいですね。
wabt の wasm-objdump コマンドを使って、WebAssembly バイナリ表現 calc.wasm ファイルの内容をダンプしてみます。かなり読みやすくなりましたが、次で別のコマンドも紹介しておきます。
wabt の wasm-opcodecnt コマンドを使って、WebAssembly バイナリ表現 calc.wasm ファイルの内容をダンプしてみます。バイナリを構成する部品を細かく見ることができます。
2.7.2 前節の calc.wasm と同じ内容の WebAssembly を、C 言語のプログラムで生成する
本書のコンパイラは、字句解析から意味解析までのフロントエンドと、中間コード生成以降のバックエンドに大別できます。 中間コード生成を起点として動作することを可能にすることで、コンパイル対象の言語の構文から独立してバックエンドの開発を試すことができるようになります。
WebAssembly は可変長の項目が多いため、少しずつファイルに書いていったら出力するのは楽なのですが、 すべてメモリ上で組み立てて完成した WebAssembly のモジュールをファイルに出力します。構文解析から JIT 実行まで、メモリ上で処理を完結させたいためです。 記号表(TP_SYMBOL_TABLE 構造体)の TP_WASM_MODULE member_wasm_module; で組み立てます。WebAssembly のモジュールは、1 つ以上のセクションで構成されているため、 TP_WASM_MODULE_SECTION** member_section; でセクション単位に組み立てます。組み立てた全セクションを結合して、 TP_WASM_MODULE_CONTENT* member_module_content; に WebAssembly バイナリ表現が構築されます。
前節の calc.wasm ファイルは、以下のような WebAssembly テキスト表現を wabt の wat2wasm コマンドを使って、WebAssembly バイナリ表現に変換したものでした。
wabt の wat2wasm で出力した calc.wasm ファイルと、C 言語で自作した中間コード(WebAssembly)生成で出力した int_calc.wasm ファイルが完全に一致するようになれば、 完全ではありませんが、中間コード生成の開発やテストが進んだことになります。 バイナリ・ファイルの比較には、Beyond Compare を利用しています。

コンパイル対象のソースコードの内容と対応する、最も重要な Code セクションを生成する手順を見ていきましょう。 可変長の項目が多いため、まずは WebAssembly コードのサイズを取得するようになっています。
取得できた WebAssembly コードのサイズから、Code セクションのバッファを確保します。 他のセクションも、TP_MAKE_WASM_SECTION_BUFFER マクロでバッファを確保するようになっています。
TP_MAKE_WASM_SECTION_BUFFER マクロで確保した Code セクションのバッファに、WebAssembly コードを生成します。
以下は、引数を持たない 1 バイトの命令(オペコードのみで、オペランドなし)の場合の WebAssembly コード生成関数です。引数 buffer が NULL の場合、 コードのサイズを返すのみで、WebAssembly コードを生成しません。TP_WASM_OPCODE_END は、Code セクションの終わりを意味します。それ以外の命令は、 スタック上の値を取り出して計算する演算子の役割りがあります。
以下は、符号付き整数を引数に持つ 1 バイトの命令の場合の WebAssembly コード生成関数です。符号付き整数をスタックに積む命令の例です。引数 buffer が NULL の場合、 コードのサイズを返すのみで、WebAssembly コードを生成しません。TP_MAKE_SLEB128_CODE マクロの内部で、次項で説明する可変長整数の LEB128 にエンコードする関数を呼んでいます。
以下は、符号なし整数(ローカル変数の番号)を引数に持つ 1 バイトの命令の場合の WebAssembly コード生成関数です。 前項でも説明しましたが、set_local は、スタック最上部の値を取り出してローカル変数に代入します。 get_local は、ローカル変数の値をスタックに積みます。tee_local は、スタック最上部の値を取り出してローカル変数に代入し、同じ値をスタックに積みます。 引数 buffer が NULL の場合、コードのサイズを返すのみで、WebAssembly コードを生成しません。TP_MAKE_ULEB128_CODE マクロの内部で、 次項で説明する可変長整数の LEB128 にエンコードする関数を呼んでいます。
2.7.3 可変長整数 LEB128 のエンコード
WebAssembly で i32.const 100 の 100 が 0xe4 0x00 に変換されるのは、符号ありの LEB128 であることが理由です。可変長整数 LEB128 は、 デバッグ情報の仕様 DWARF などでも使われています。大きなサイズの型のデータを固定長で確保してしまうと、バイナリのサイズが膨れ上がります。小さな値が使われる頻度が高ければ、 LEB128 でデータを可変長で確保すれば、バイナリ・サイズの削減に効果的です。 以下のソースコードは、 int_calc_compiler/src/lib/tp_compiler/tp_leb128.c からの抜粋です。int64_t 型の値を LEB128 に変換する関数と、uint32_t 型の値を LEB128 に変換する関数です。引数 buffer が NULL の場合、 変換後の LEB128 に必要なバッファのサイズのみを返し、バッファに LEB128 を出力しません。また、符号付きの整数型の負の値の右シフトが、 算術右シフト(符号拡張される)として処理されることが前提になっています。論理右シフト(ゼロ拡張される)として処理されるコンパイラでは正しく動作しません。
2.7.4 構文木と意味解析の登録情報から、WebAssembly を生成する
中間コード生成では、構文解析が出力した構文木を読み込んで、意味解析が出力したハッシュ表で名前を検索したりすることになりますが、 意味解析の場合と同様に、深さ優先探索で構文木をたどっていくことで実現します。
中間コード生成では、構文木をたどっていきますが、意味解析と同様に正しい文法のデータであるか検査するようになっています。 エラーの場合にコンパイルを停止します。この場合、構文解析と中間コード生成で処理内容に相違があるということですから、 内部コンパイラ・エラー(ICE: Internal Compiler Error)となります。
本書の構文木は、非終端記号や括弧が含まれるため、一般的に解析木や具象構文木と呼ばれているものです。中間コード生成では、文法:Factor -> '(' Expression ')' の括弧は読み飛ばします。
WebAssembly の Code セクションのローカル変数の数に、意味解析で記号表(TP_SYMBOL_TABLE 構造体)の member_var_count に保存していた値を使います。
コンパイル対象のソースコードの最後の文で、set_local と get_local の組み合わせの代わりに tee_local を生成します。生成されるコードを短くし、スタックに計算結果を保存できます。 意味解析で記号表の TP_PARSE_TREE* member_last_statement; に保存していた文を、最後の文として参照します。 set_local/get_local/tee_local で必要なローカル変数の番号は、意味解析が出力したハッシュ表で、構文木に含まれる名前を検索することで、取得できます。 記号表の member_code_body_size に生成した WebAssembly 命令のサイズを加算していくことで、WebAssembly の Code セクションのサイズを計算します。 また、記号表の uint8_t* member_code_section_buffer; に WebAssembly の Code セクションを生成していくようになっています。複数の関数に処理が分かれているためです。
この章を執筆している時点では、WebAssembly に整数の符号反転や NOT ビット演算の命令が用意されていなかったため、 ローカル変数と -1 の排他的論理和(XOR)した結果に 1 を加算することで、符号反転を実装しています。
最終更新