VSCode拡張の簡単なアセンブラのデバッガをOCamlで作った話


これは ML Advent Calendar 2017 の 4日目の記事です。VS Code も絡むのでそっちにも登録してしまいましたw

はじめに

MLを使いならば、操作敵意味論を理解しなくてはならない。
操作敵意味論を理解するには、論理型言語を理解しなくてはならない。
MLでPrologを実装できれば論理型言語を理解していると言ってよいはずだ。
論理型言語の動作を理解するためにはデバッガがあると便利なはずだ。
しかし、デバッガを作るノウハウが我々にはない。
よって、我々はOCamlによるデバッガを作るサンプルを作成することにしたのである。

1. VS コードのデバッガサンプル(PHPバージョン)

これは簡単なアセンブラ言語を作ってそのデバッガを作るサンプルプロジェクトである。
通常は、TypeScriptでDebug Extensions を書くのですが、簡単な標準入出力で VS Code Debug Protocol を使ってデバッグ用のサーバが書けそうなので、Debug Extensions を PHP で書いてみたというものだ。

デバッガを試す場合に、リロード作業が必要ないのでデバッガのデバッグをPrintfデバッグしながら開発したい場合に便利である。
また、初めてのデバッガ開発だけどどうしたら良いのかよくわからないという場合に、直接VSCodeのプロトコルを使うことを検討したい場合にも役立つ。

以下のようなアセンブラをデバッグしてみることができる:

asm.txt

main:   call addp 1 2 a
        print a
        ret 0

addp:   enter a b
        add a b c
        ret c

1.2 デバッガの構成

  • asm.txt : デバッガでデバッグできるアセンブラのソースコード。
  • package.json : VSCode の拡張の設定が書いてある。
  • src/extensions.ts : VSCodeに読み込まれる拡張。 out/src.js に展開さる。デバッグサーバのプロセスを起動し標準入出力を使ってデバッガを動かす。ほとんど何もしない。
  • dist/server.sh : デバッガのサーバは外部プロセスだ。dist/server.php を呼び出し、標準エラーをファイルに吐き出す。
  • dist/server.php : デバッガの本体だ。言語機能そのものも内包している。VSCodeのデバッグ時にプロセス起動される。
  • src/server.ts : こちらはTypeScriptによるデバッガの本体だ。これは無くてもデバッガは動作する。
  • client.php : デバッグサーバに接続してメッセージを送り受け取ることができる。デバッガのメッセージ内容を把握するのに役立つ。

1.3 実行方法

npm install
make ts
make

とすると、このプロジェクト自体が ~/.vscode/extensions/asm-php-0.0.1 にコピーされる。

最初は一度再読込が必要だ。 F1 を押した後、ウィンドウを再読込む。

2回目以降は、外部プロセスのみの書き換えになるのでも再読込なしで実行できる。
また、server.shを書き換えることで、読み込みプログラムをphpからjsに書き換えることも可能だ。

デバッグコンソールに3と表示されれば成功である。

make または、make tailを実行しておくとデバッグサーバの標準エラー出力が出力結果がターミナルに表示されてログとして利用できるので便利である。ただし、Unix環境でないと動かないかもしれない。
makeによるログ表示はtailコマンドで/tmp/server.logを表示するだけなので表示しなくても動作する。

次は asm.txt を開き、ブレークポイントを設定しデバッグしてみよう。
プログラムを停止したあと変数の中身を書き換えることもできる。

1.4 アンインストール

make uninstall

とすると、拡張は削除可能だ。

2. TypeScript での言語拡張

VS Code の言語拡張は基本的に、TypeScriptで作るようにドキュメントが整備され、ライブラリが整備されている。したがって我々は、まず、TypeScriptを使って拡張を作成することとした。できるだけ簡単にするため作る言語はアセンブラであり、add,sub,mul,div,print,call,enter,retなどがある言語として実装を行った。
とりあえずのプロトタイプなので、パーサは1行ずつ読み込んで空白でSplitしたものを使うという原始的なものであった。
しかしながら、しっかりと動作するように、通常の実行、ステップオーバー、ステップイン、ステップアウト、変数の設定、ウォッチ式などを動くように作成した。

主なプログラムはここから参照できる。

2. PHP での言語拡張

PHPによる言語拡張は、TypeScriptによる言語拡張の機能を必要な部分だけ取り出して移植したものである。PHPを選択した理由は比較的 TypeScript や JavaScript に似ている文法であり、移植しやすいとの判断による。筆者が使い慣れているのも大きかった。
JSONの読み書きが簡単にできるし、PHPで出来ないことは高度なことであろうからということもあった。

仕様がはっきりしているならば、OCamlに直接移植した方が良かったのかも知れない。
しかし、APIの解析を行いつつ、オブジェクト指向言語から関数型言語への移植のためにアルゴリズムの修正まで行うのは結構大変そうだったので一度PHPで作成してみた。

このサーバプログラムは単体でも動作するPHPプログラムに過ぎない。APIの中身で何をやっているかもよくわかった。
3日ほどかかった。

3. OCaml での言語拡張

いよいよ、本命であるOCamlへの移植である。PHPに移植したおかげでAPIの中身はわかりきっている。後はただ移植するだけである。

問題となったのはJSONの読み書きをどうするか、パーサはどう作るか、APIをどう表現するかであった。

3.1 OCamlでのJSONの使用方法

OCamlには様々なJSON用ライブラリがあるが、高速で便利そうなYojsonを使うことにした。

OPAM によりYojsonをインストールして後は使ってみるまでが苦しかったけど、ちょっと使い始めて慣れてしまえばしっくりと来てとても使いやすかった。

次の問題はパーサである。文法定義で結構悩んだ。あまりネット上でアセンブラのBNFは見つからなかったので自分で考えることにした。最終的には以下のような文法になった:

program:| insts                 { $1 }
insts:  | label EOF             { [] }
        | label inst EOF        { ($1,($2,$3))::[] }
        | label inst EOL insts  { ($1,($2,$3))::$4 }
label:  | ID COLON eols         { $1 }
        | eols                  { "" }
eols:   | /* empty */           { () }
        | EOL eols              { () }
inst:   | ADD imm imm reg       { Add($2,$3,$4) }
        | SUB imm imm reg       { Sub($2,$3,$4) }
        | MUL imm imm reg       { Mul($2,$3,$4) }
        | DIV imm imm reg       { Div($2,$3,$4) }
        | CALL ID imms          { call $2 [] $3 }
        | ENTER regs            { Enter($2) }
        | PRINT imm             { Print($2) }
        | RET imm               { Ret($2) }
imms:   | imm                   { [$1] }
        | imm imms              { $1::$2 }
regs:   |                       { [] }
        | reg regs              { $1::$2 }
reg:    | ID                    { $1 }
imm:    | reg                   { Reg($1) }
        | INT                   { Int($1) }

VMの中身は破壊的な構成をそのまま移植したのであまり美しくない。
しかし、OCamlは実用的な言語なので問題なく移植できた。

デバッガのAPIはモジュールを使ってインターフェイスを定義して使うことも考えたのだが、1つしか実装しないのにわざわざモジュールを使ったテクニックを使う必要もないだろうということで、コアな機能を持ったモジュールと、それを使った実装のモジュールと、メインのディスパッチ処理をするモジュールの3つに分けて実装した。おかげで、必要最小限の知識で読み解くことができるようになった。

この移植作業に1日ほどかかった。
そして、型付きのMLで作ったのだからすぐに動くことを我々は期待した。
しかし、そううまくはいかなかった。嫌になって我々は眠りについた。

2日目に、気を取り直してデバッグを行った。Yojsonは型がゆるいので結構なエラーを吐いていたので対策した。最初は戸惑ったが、try withで例外を補足してデフォルト値を返すようにするなどの対策をすれば良いことがわかった。

行情報は普通に1行目から始まるようにパーサを作ったので変換は不要になった。
また、配列と参照を使ったアルゴリズムをリストを使った物に書き換えるなどした箇所でも挙動がおかしな部分もあったので、破壊的な変更のない再帰的な関数に書き換えるなどして対応すると安定動作するようになった。

OCaml拡張による成果物は以下を参照されたい:

3 まとめ

我々は、VS Code による拡張を作成した。TypeScriptをPHPに移植し更にOCamlに移植してリファクタリングを行った。
このノウハウがあれば、OCamlを使って作成した言語のデバッガを作成することが簡単にできるはずである。
今後はこのノウハウを生かした本格的なデバッガを作成していきたい。