はじめに:大反響への感謝
前回の記事「うっかり世界最強のWasmコンパイラを作ってしまった件」では、予想を遥かに超える反響をいただき、なんとQiitaの週間お気に入りランキングで2位を達成しました!
累計PVも45,000PVを突破し、GitHubリポジトリ(hike-lang)のスター数もあっという間に50を超えました。記事を読んでくださった皆様、温かいフィードバックをくださった皆様に心より感謝申し上げます。
これほど多くの方に注目していただけるプロジェクトになった以上、生半可なものは出せません。
ここ1週間ほどは開発の手を一切緩めず、コンパイラの全貌を見直す徹底的なリファクタリングを敢行し、言語処理系としての安定性を極限まで高めていました。
その結果――「Hikeコンパイラの全コードを、Hikeコンパイラ自身でコンパイルする(セルフホスティング)」に無事成功しました!
PCネイティブ環境(Windows/Linux等)向けとしてはすでに完全動作していますが、WebAssemblyとして自己完結して走らせるにはまだ少し課題が残っているため、セルフホスト版の全体公開については今しばらく楽しみにお待ちいただければ幸いです。
ブラウザWasmという「黒魔術」と、開発者の絶望
……と、景気のいい話を並べましたが、ここで一度エンジニアの皆様に胸に手を当てて問いかけたいことがあります。
「ぶっちゃけ、ブラウザでWasmを運用するのって死ぬほど大変じゃないですか?」
ブラウザ上でWebAssemblyを直接動かすのは本当に骨が折れます。生のWasmバイナリをただ読み込むだけでは何もできず、手厚いJavaScriptのグルーコードやランタイムで何重にもくるんであげなければなりません。
文字列を1つ関数に渡すだけでも、JavaScript側でエンコードして線形メモリへバイトバッファとして書き込み、ポインタと長さをちまちま手渡しする。関数の処理結果を受け取るためにも、リニアメモリの該当オフセットからちまちまバイト列を掘り出す。お世辞にも「快適な開発体験」とは言えません。
そして一番の絶望は、そこまで苦労して呼び出したWasmが、プログラマーの想定通りに動かなかった時です。
ブラウザやWebAssemblyの仕様上、適切なデバッグ情報(DWARF)が付与されていればDevToolsでのソースレベルデバッグ自体は可能です。しかし、自作言語であるHikeには、当然ながらClangやRustのような巨大な既存ツールチェーンの恩恵はありません。
LLVM等の既存エコシステムをバイパスして独自バックエンドから直接バイナリを吐く場合、デバッグ情報がなければWasmの中身は16進ダンプが詰まった完全なブラックボックスです。「今、どの行を実行していて、どの変数が狂ったのか?」を追いかけるのは極めて困難で、有益なエラーログすら出ずに沈黙するブラウザの前で、ただ立ち尽くすしかありません。
「Chrome DevToolsのデバッガーで、ふだんのJavaScriptと同じように、F12キー一発でWasmの中身を1行ずつ覗き見られたらどれだけ幸せだろうか……」
これは、私がWasmの開発に足を踏み入れた当初から、長年ずっと胸の奥底で燻り続けていた切実な悲願でした。
おかげさまで前回のWasm記事は大いにバズりました。しかし、どれほど生成バイナリが小さく速くなろうとも、自前の直接生成パイプラインにおいてWasmが「中身の見えないブラックボックス」である現実は何ひとつ変わっていなかったのです。
「Wasmの中身を、JavaScriptとまったく同じようにブラウザ上でステップ実行したい。既存のツールがないなら、自分で作るしかない」
長年抱えてきたその執念をついに実行へ移す決意を固めました。
――ところが、そこからが本当の地獄の始まりだったのです。
なぜLLVMだけでなく「WAT直接出力」が必要だったのか?
従来のHike言語は、中間表現からLLVM IRを出力し、Clang/LLVMの最適化パイプラインを通してネイティブ実行ファイル、共有ライブラリ(.dll/.so)、そしてWebAssemblyをビルドする仕組みを取っていました。
しかし、今後HikeがWebAssemblyのポテンシャルを100%引き出し、よりネイティブかつゼロオーバーヘッドにWasmを操るためには、WebAssemblyの標準テキスト表現であるWAT(WebAssembly Text Format)を直接生成し、公式のWABT(WebAssembly Binary Toolkit)等でWasmへ変換する第2のバックエンドがどうしても必要だと考えました。
LLVMの巨大なブラックボックスを介さず、S式で表現されるプリミティブなWAT命令をコンパイラが自前で直接生成する。
LLVMバックエンドで培ってきた200件以上のE2EテストスイートがWATバックエンドでも1件の狂いもなくパスした瞬間、確信しました。「これでWATのコード生成品質は完璧に仕上がった」と。
そして、エンジニア特有の欲が湧き上がってきたのです。
「せっかくWATを生で直叩きできるなら、Chromeの開発者ツール(DevTools)で元のHikeコードを直接開いて、F12キー一発で1行ずつブレークポイントデバッグできたら最高に気持ちいいんじゃないか?」
泥沼のDWARF自力実装:丸一日以上溶けた「魔境」の記録
通常、Wasmのソースレベルデバッグといえば、ClangやRustなどの巨大なエコシステムがLLVMのDIBuilderを介してDWARFデバッグ情報を出力してもらうのが業界の常識です。
WebAssembly公式のTool ConventionsやChromeの技術資料にDWARF埋め込みの仕様こそ存在しますが、入門書はもちろん、「自作言語の独自バックエンドからDWARFセクションそのものを組み立てる」具体的な実装例となると、参考資料は一気に少なくなります。
「LLVMに頼らず、コンパイラ自身が直接DWARFバイナリ(.debug_info、.debug_line、.debug_abbrev、.debug_str)を自前で1バイトずつ編み上げてWasmカスタムセクションにパッキングする」という無謀な挑戦が始まりました。案の定、開発は一筋縄ではいかず、丸一日以上パソコンの前に張り付いて唸り続けることになりました。
1. DevToolsの「無言スルー」という精神的試練
DWARFはLEB128可変長整数や命令オフセットがひしめく純粋なバイナリ工学の世界です。
構文エラーの親切なメッセージなど一切出ません。URL解決のハンドシェイク、略記テーブル(abbrev)のタグ階層、バイトオフセットが1バイトでも狂っていると、Chrome DevToolsは何のエラーも吐かずにただ静かにSourceタブから消え去ります。Wasmバイナリを16進ダンプで睨みつけながら、1バイトのズレを特定する作業が延々と続きました。
2. 行番号の「反復横跳び」問題
ようやくDevToolsに元のmain.hikeが表示され、ステップ実行ボタン(Step Over)を押した瞬間、頭を抱えました。
カーソルがスムーズに次の行へ進まず、45行目→46行目→45行目と、小刻みに行番号が反復横跳びしたのです。
原因は、中間命令(HIR)の展開単位で律儀に行情報を出力していたことでした。さらに、関数の脱出時にCFGループのディスパッチヘッダへ一瞬吸い寄せられるWasm特有の構造も重なっていました。そこで行テーブル(.debug_line)のステートマシンを根本から改修し、連続する同一ソース行の命令群を1つに集約するアルゴリズムを組み込むことで、高級言語として自然な「1行ステップ実行」を実現しました。
3. 変数スコープのノイズと「戻り値」の救出
DevToolsの右ペインにある「Scope」に変数を表示させようとした際、最初はコンパイラ内部の仮想SSAレジスタ(v3, v4...)が数十個も溢れ出し、肝心のユーザー変数が埋もれる事態に直面しました。
ユーザーが定義した変数と引数だけをピンポイントでDIE(Debug Information Entry)に登録し、さらにWasmの線形メモリやローカルインデックス(DW_OP_WASM_location)へ正確にバインド。仕上げに、関数を抜ける直前の戻り値(return_of_function)も暗黙的にスコープへ露出させることで、完璧に直感的なインスペクションを完成させました。
完成:ブラウザで開いてF12を押すだけの世界
苦闘の末に完成した画面がこちらです。
- Chrome標準のDevToolsでそのまま動作
- 特別なSourceMapプロキシサーバも不要
-
-gをつけてコンパイルするだけで、ブラウザを開いてF12を押せば、Sourcesツリーに元の.hikeコードがそのまま現れる - 行ブレークポイントで止まり、ステップ実行ができ、右側の「Scope」には引数
a: 1234、b: 5678、そして計算されたreturn_of_function: 6912がリアルタイムに表示される
そして驚くべきはバイナリサイズ:「わずか7.43KB」
これほど高精度なDWARFデバッグシンボル、行番号テーブル、変数情報をすべて詰め込んだ状態(デバッグビルド)であるにもかかわらず、Wasmバイナリのサイズはわずか7.43KBしかありません。
Go言語のWasmなら数MB、RustやC++でも数十〜数百KBに膨らむのが当たり前の世界で、7KB台。一般的なTCP初期ウィンドウ(RFC 6928 IW10: 約14.6KB)の範囲内なら、Wasm本体7.43KBは最初の往復(RTT)でブラウザに届き得ます。デバッグ情報込みでも7.43KBに収まり、転送サイズへの影響を非常に小さく抑えられました。
オンラインデモを公開しました
この「F12を押すだけで自作言語がブラウザ内でネイティブデバッグできる体験」を実際に触っていただけるデモページを用意しました。
👉 Hike WebAssembly DevTools デモページ
Chrome(またはChromium系ブラウザ)でアクセスし、F12キーを押して「Sources」タブを開いてみてください。Wasmバイナリから復元された元のHikeソースコードが現れ、ボタンをクリックすると設定されたブレークポイントで綺麗に停止し、変数の中身を確認できるはずです。
おわりに:Hike言語が手に入れたもの
今回のアップデートにより、Hike言語は以下の強力なエコシステムを完全に確立しました。
-
ネイティブ & Wasmの完全デュアルデバッグ:
CPUネイティブ(VSCode / GDB / LLDB)でも、WebAssembly(Chrome DevTools)でも、-gフラグ1本で全く同じ感覚のソースレベルデバッグが可能。 -
ゼロ構成のブラウザ統合:
WasmとJavaScriptの相互呼び出し(cfunc/jfunc)を記述するだけで、グルーコードとなるruntime.jsをビルド時に自動生成。手作業のメモリマーシャリングは一切不要です。 -
完全なゼロランタイム(Wasm側):
ここでいう「ゼロランタイム」とは、GC・VM・言語スケジューラ等の常駐ランタイムをWasm側に要求しないという意味です。ブラウザAPIとの接続にはHikeが自動生成する薄いruntime.jsホストブリッジを使用し、ブラウザが元から持っている機能はWeb API側に委ねつつ、Wasm本体に巨大な共通ランタイムを持ち込まない設計を貫いています。Cランタイム(libc)や重厚なWASIも不要で、最小のフットプリントで動作します。
「最速の生アセンブリを叩く硬派なシステム言語」でありながら、「初心者がブラウザでF12を押すだけで直感的に動かせる最高の開発体験(DX)」が共存する環境が整いました。
ブラウザで動く超軽量・高速なWasmを作りたい方、低レイヤの処理系開発に興味がある方は、ぜひリポジトリを覗いてみてください!
👉 **GitHub: kanryu/hike-lang**

