HSP3 でスクリプト言語の処理系を書く

HSP3 は BASIC 風の構文を持つスクリプト言語で、GUI プログラミングがやりやすいことに人気がある。私は10年ぐらい前に、どういうわけかその HSP3 でインタプリタを書いて失敗した。

10年ほど経って問題の解決策が分かってきた感じがしたので、改めて似たようなものを作ってみた。まだ10日ほどしか作業しておらずプロトタイプ段階といえるが、頭の中にあった解決策が実際に問題を解決できているという実感が得られた。

klac-lang の失敗

klac-lang とは、私が2008年9月ごろに書き始めた JavaScript っぽい自作言語のインタプリタ。これについて詳しく書きたくないので簡単にまとめると、以下のような点で失敗した。

  • バージョン管理していなかった
    • バグを埋め込んでしまったら、それを直すまで作業が進まなかった
    • バックアップ (日付ファイル名の zip ファイル) もまばらすぎた
  • テストコードを書いていなかった
    • 書き換えるたびに至るところからバグが噴出していた
  • 演算子優先度順位法っぽいが異なる、筋の悪い方法で構文解析をしていた
    • ルールの整合性が取るために長時間の試行錯誤が必要だった
  • データ構造としてモジュール変数やプラグインを多用していた
    • それらを使っても、それほど表現力が上がるわけではない
    • コードは読みづらくなり、パフォーマンスや移植性にも悪影響があった

結果として、標準ライブラリの読み込みに数秒かかるほど遅く、動かすたびにバグが見つかるという、触っていてうんざりする代物になってしまった。

学び

  • バージョン管理:
    • Git が使えるようになった
  • テストコード:
    • テストコードを書いて挙動を安定させる作業にそこそこ慣れてきた
  • 構文解析:
    • 再帰下降パーサーを手書きできるようになった

学び: データ構造

HSP3 で現実的に使えるデータ構造は基本的な型 (label/str/double/int) の配列ぐらいしかない。

COM オブジェクトやプラグインやモジュール変数を使うという手もあるが、HSP3Dish や Linux 版などでは実装されていない。移植性の問題だけでなく、上記4つの基本的な型以外は命令・関数の引数に指定したり関数の返り値にできないなど、制限がある。

そのため、配列を工夫して使うほうがよさそうだ。

例えば、複数の配列を組み合わせて、オブジェクトの配列のようなものを作れる。つまり、複数の配列の i 番目の要素がオブジェクト i のフィールドであるということにすれば、これらの配列はオブジェクトの配列であるかのように扱える。また、要素番号 i をオブジェクトへの参照とみなせば、再帰的な構造も作れる。

これだけでインタプリタの実装に必要なデータ構造を十分に実現できる。

この方法では、オブジェクトの解放は自前で実装する必要がある。インタプリタなら GC は実装することになる (むしろ実装したい) のでその点は仕方ない。

そういうわけで上記の問題の解決策が分かった。

negi-lang の成功(?)

negi-lang は、2019年2月になって再び書き始めた JavaScript っぽい自作言語のインタプリタ。

作業開始から9日目の時点で、整数の四則演算、ローカル変数、if文、while文、配列、クロージャ、外部関数、GC (配列のみ) あたりが実装できている。

外部関数というのは、呼び出したら HSP 側のスクリプトで何らかの処理が行われて、その結果が返ってくる、みたいな仕組みで動く関数。これがあれば HSP でできることがだいたいできるようになる。実際 mes や button などの命令を外部関数として登録してやることによって簡単な GUI アプリができることは確認した。

(オブジェクトがまだないので JavaScript っぽいというと語弊がある。構文は似ているが、アロー関数はない。)

negi-lang のコードはいまのところ十分に手を入れやすく、klac-lang のような泥沼ではない。他の機能を入れるのもおそらく困難ではない。

というわけで klac-lang の供養ができた。

未解決問題

このインタプリタに使いみちがないという問題は解決できてない。

その他

  • linter-hsp3 がとても便利だった
    • 特に HSP でもっともストレスフルな瞬間、「error 26 : パラメーター引数名は使用されています」を事前に教えてくれるので気分的に楽になる
  • はじめ、Rust のように式の中に文を書けるような文法にしていたが、break などのジャンプ命令によってスタックの構造が壊れてしまうということを知った
    • 例えば while (p()) { s += (if (q()) { break } else { 1 }) } みたいな式だと、+= の左辺の s がスタックに乗った状態で break に到達してしまうので、ジャンプする前にそれをポップする必要がある。
    • レジスタマシンなら問題ない?
  • enum で定義した定数をうっかり別の系統のと混ぜてしまうバグが、「定数値がいまは偶然同じだから動いてる」ような状態になってないかな? と思って、定数値がすべて重複しないように変更してみたら、案の定そういうミスが見つかったということがあった https://github.com/vain0x/negi-lang/commit/8d668b95b8e5884bcac995f57e045d90fab14dee

関連記事