📝

自作コンパイラのTUIデバッガーを作った

に公開2

はじめに

以下の記事を見て、いいなと思ってやってみた

https://zenn.dev/msakuta/articles/723ee6ae3b7eca

作ったもの

ソースコード(参考程度に)
https://github.com/myuon/iolite/blob/main/src/tui_debbuger.rs

機能としては以下のようなものがある

  • ソースコードの表示
  • disassembleの表示
  • スタックフレーム、スタックの表示
  • ラベルとアドレスの表示
  • ブレークポイントの設定、1命令の実行、resume、次のcall/returnまでの実行(step out相当)

技術選定と大まかな設計について

TUI部分は元記事と同じくratatuiを採用した。使いやすくてよかった。

デバッガーのコンパイラ部分については、もともとDAPサーバー用にブレークポイントの機能などは用意していたのでそれをほぼ流用しているため、今回のために必要な実装はそこまで多くはなかった印象。

むしろ、上記のソースコードやdisassembleの表示(&現在の実行箇所の把握とか)のために手を入れたところの方が多いかも。

基本的な設計としては、(DAPサーバーと同様に)デバッガー起動時にランタイムごとメモリに乗せて、実行しながらランタイムのAPIを外から呼び出していくような形としている。

ただ、Runtimeはメモリなどを触る都合でSync,Sendを満たさない形であり、デバッガーからの操作によってUIをブロッキングしたく無い関係で Arc<Mutex<Runtime>> してしまっている。今のこの言語は単純なので並行並列処理に関するprimitiveなどはないからいいものの、そういうのを本格的に触りだすと多分これPython同様にGlobal Interpreter Lockが問題になるやつでは…と地味に頭を抱えている。
(現状はそこまで実用的な言語にする予定はないからいいけど)

ソースコードの表示

ソースコードの表示は単にソースをとってきて表示すればいいかと思いきや、syntax highlightがやはり欲しいよねというところでそれ用の仕組みを入れている。
LSPの実装時にASTをtraverseしてsemantic token(要は、ASTを見ないとわからない系のトークン情報。例えば与えられた識別子がclosureを表す変数の場合は、関数を表す識別子と同様のhighlightにしたいとか)を返す実装をやっているので、そこの実装を流用した。どうせTUI上でそこまでリッチな見た目にはできないので、キーワードとかリテラルとかコメントとかの一部だけ簡単に判別してそこだけ色を変えるような実装になっている。

Disassembleの表示

これもこのデバッガー用に用意した。今の言語は独自の命令列をbinaryで吐いてそれをVMランタイムに食わせて実行する形だったので、コンパイルされたbinaryからアセンブリ命令列に戻す部分を作ってそれを表示している。

StackFrameとStackの表示

これは簡単でメモリを見てよしなに解釈をするだけ。
今の言語はインタープリター寄りでメモリ上の表現に簡単な型情報が載っているので、メモリアドレスなのか数値なのかを判断して表示を変えたりしている。StackFrameについてもspとbpを見てよしなに解釈をすれば良いので簡単。
Stack viewではStackframeの切れ目のところで背景色をつけてわかりやすくしている。

実装が簡単な割に、デバッグ時にこれがあるとめっちゃくちゃ捗るので、スーパー便利機能としていくつかのデバッグ作業時に大変活躍した。
(デバッガーがない時のデバッグでは、1命令実行するたびにスタックの状態を出力しておき、それとソースコードとをにらめっこするみたいな作業をずっとしていたので)

ソースコード上の現在位置の表示

もう一つデバッガーのためにやっていることとして、「今実行しているのがソースコードの何行目に当たるか?」のハイライトがある。
これについては、コンパイルする時にソースコードのstatementごとにfile名と行数を表すだけの命令を生成されたコードに埋め込んでいる。(あくまでstatementの1行について1つ命令を埋め込むだけなのでそこまでデバッグ情報まみれになるわけではない。)
そして、VMランタイムでの実行時にはその命令は無視するが、最後に通過したファイル名と行数を覚えておいてデバッガーでそれを参照して適切な箇所にハイライトする、という実装を行なった。

また、本来はmainを実行する時に標準ライブラリの中のソースを実行している瞬間などもあるので、これらについても今フォーカスするべきファイルを開き直してあげたり、あるいはモジュールごとに最後にいた行数を覚えておくことで、「今はstdの方にいるがさっきまではmainの何行目にいた」をハイライトしてあげるのも良いかなと思っている。
(現状は、実行する時にmainからstdにうつってしまうとmainの方のハイライトは無くなってしまうので)

終わりに

全ての自作コンパイラ勢は、エディターのsyntax highlightサポートと同じくらい重要な要素としてTUIデバッガーをすぐに作った方が良い、と言い切れるくらいにデバッガーは便利な存在と言える。

個人的にはTUIというのも十分なカスタマイズ性とほどほどな取り回しの良さがあり、同時に複数のアイテムを見なければならないデバッガーというソフトウェアを作るに必要十分な要素を備えているというのもよかった。
という観点で、msakutaさんの記事に出会えたのはとてもよかったと思っている。

今までずっとランタイムの貧弱な出力と睨めっこしていたのがバカらしくなるくらいの文明的な発明なので、ぜひ各位もデバッガーの作成をやりましょう。

あとついでにLSPも作りましょう。こちらもあると大変便利なので。(個人的な意見です)

Discussion

msakutamsakuta

引用元記事の作者ですが、すばらしい TUI デバッガですね!

私はこの言語のことは存じ上げませんが、スクリーンキャプチャを見るだけで中間コードを実行する VM であることと、可変長命令であること、定数を即値で埋め込んでいることと、スタックベース実装であることが分かります。一目見ただけでそれだけ掴めるということは、内部情報の可視化と理解に役立っているということです。デバッガという名前はミスリーディングですが、バグが無くても積極的に実装・使用していきたいですね。

私の記事で技術選定の理由に書き忘れたことがありまして、 Ratatui は単純なシングルスレッドのイベントループなので、 async ランタイムのようにデータに Sync や Send を要求しないということがあります。私の言語では VM 内部に Rc を使っているので、 Tauri などを使ったとしたら Arc に書き直さないといけないところでした(デバッガのためだけに!)。

LSP 対応は私もしたいと思っているところですが、後回しにしてしまっています^^;

myuonmyuon

見ていただいてありがとうございます!

元の記事に影響を受けて作ってみたと言うところではあるのですが、元記事だとタイムトラベル機能なども実装されていてかなりすごいなと思いながら拝見しておりました。

デバッガと並び、自作言語においてはLSP対応もかなりDX体験に寄与するのでお時間あればおすすめです。情報がなかったり不安定な印象があるDAPの実装と比べても、ある程度エディター側の挙動なども安定しておりそこまで苦労なくできるかと思います。