はじめに
この文書は、lua で作成した Forth 系言語 F/L を作る時に考えたことをメモしたものです。この自作言語は、私にとっては言語処理系の習作という位置づけであり、この程度の規模であっても、言語処理系らしきものを作るのは初めてです(厳密に言えば、おもちゃのような言語処理系を学生の頃に作ったことがありますが、それも最早 20 年以上前となります)。実装が楽な Forth 系言語とはいえ、プログラミング言語を作るときには、こんなことを(こんなことまで?)考えるのだなぁ、という記録になれば幸いと思い、記事にしました。
様々な種類のデータを取り扱う
F/L は Lua で実装されています。以前書いた文書「35 行の Lua プログラムで Forth インタプリタを作る(内部インタプリタ 6 行、外部インタプリタ 29 行)」で述べたとおり、スタックには Lua で取り扱うことのできる、数値(浮動小数点値)、文字列、無名ワードブロックなどを格納することができます。F/L では、これらのデータ型を第1級オブジェクトとしました。これにより、従来の Forth システムを見直すことができました。特に文字列の導入により、ワード名の指定が外部インタプリタと連携することなく、内部インタプリタにより実行される「閉じた世界」で完結できるようになりました。
ワード : (コロン)の廃止
従来の Forth では、: (コロン)というワードは自身の後に記述されている文字列を読取り、その文字列をワード名とする新たなワードを定義します。考えてみると、これは少し奇妙な動作です。
ワードは内部インタプリタにより順次実行されるプログラムです。一方、ワードを起動するために入力された文字列を管理・解釈するのは外部インタプリタの仕事です。つまり、コロンというワードは、ワード名を取得するため、外部インタプリタと連携し、外部インタプリタで解釈される前の文字列を横取りしている状況です。
横取り、という表現は少々大げさだったかもしれません。実際には、ユーザーからの入力を保持している入力バッファから文字列を取得しているだけです。しかし、単なる 1 ワードである : (コロン)が外部インタプリタの入力バッファにアクセスすることは、個人的には結構気持ち悪い感じです。
ワードの世界(レイヤといっても良いかもしれません)と、インタプリタの世界は異なっている、というのが私の認識です。外部インタプリタ・内部インタプリタの世界は、ワードからみるとひとつ上のメタな世界という認識です。
そのため、F/L では Forth のコロン定義方式は採用しませんでした(ワードと外部インタプリタの分離)。
幸い F/L では文字列を第 1 級オブジェクトとしましたので、外部インタプリタから値を入手せずとも、データスタックからワード名を取得することが可能です。F/L では、 "wordName" := defines ... ; という形式にて新しくワードを定義することとしました。
ループ構造の見直し - イテレータの導入
伝統的な Forth では、ループは DO 〜 LOOP で記述されます。また、ループ変数も I, J というものであり、2 重ループ以上のものは考慮されていません。Forthは、ボトムアップ指向ですから 3 重ループが必要ならば、最も内側のループをワードとして定義し、それを呼び出しなさい…ということなんだろうと思います。
F/L では、整数以外の値もスタックに積むことが可能です。そのため、ループカウンタ変数を I や J に固定することは無理があるように思います。そのため、データスタックの TOS にループカウンタの値を積むことにし、この値が false でない間ループを実行するようにしました。
最も単純なループ用ワードとして while 〜 loop を設けました。これは、単純にデータスタックから値をひとつポップし、それが false でない間、loop までのワードを実行するというものです。
while 〜 loop では、誰かが既にデータスタックに積んだ値により、ループの継続・終了を判断します。これはループ内である条件が満たされたときにループから脱出するような場合には適した方法だと思います。
しかし、例えば 1 〜 1000 までループカウンタを増加させつつループを回す場合、1 〜 1000 までの値を、ループが回る毎に誰かがスタックに積み続ける必要があります。F/L ではこのような作業を担う存在としてイテレータを導入しました。イテレータは実行のたびごとに、更新されたループカウンタの値をデータスタックにプッシュするものとします。
イテレータを利用するには foreach 〜 next ブロックを使用します。各ループの開始時に foreach 環境スタック上にあるイテレータ(実態はワードへのポインタ)を実行し、カウンタの値をデータスタックプッシュします。ループ終了時には、データスタックの TOS の値が false になり、loop までスキップし、ループの終了となります。
環境スタックの導入
F/L ではループ中などで、複雑な処理を行うため、環境スタックを用意しローカル変数を導入しました。ワード do 〜 end でひとつの環境を環境スタック上に構築します。環境スタックはリターンスタックと同様、>E や E> というワードを用いて、値をプッシュ・ポップできます。ローカル変数へのアクセスは !var と @var で行うことができます(これらについては次節でもう少し詳しく述べます)。
可能な範囲で、Forth 文化の継承を目指す
このように、F/L は Forth には無い特徴を備えるに至りましたが、Forth から変更する必要のない部分については、できるだけ従来の Forth と同じ表現を可能とするように努めました。特に、データスタックの TOS = Top Of Stack を複製する dup や、TOS を表示する . (ドット)などは F/L でもそのままです。
Forth のワードがそのまま保存されているものもあれば、ワード名の一部にその名残を残すものもあります。
F/L には Forth と同様に値を返すワードとしての変数と、Forth には存在しなかったローカル変数の 2 種類の変数があります。これらの変数への値の格納(代入)をどのような記号にするべきなのかは、いろいろ迷いました(今も迷っています)。
簡潔さを重んじる Forth では、! で値の代入を、@ で値の参照を行いますが、もう少し分かりやすいワードは無いものかと思い、グローバル変数については set! を使用することとしました。これは scheme から拝借してきたものですが、Forth と同様に ! という記号を含んでいますので、一応 Forth の流儀は守っているといえるのではないかと思っています。
ローカル変数には !var を用いることとしました。ローカル変数からの値の取得は、Forth の文脈において !var と対を成すよう、@var で行うこととしました。
コンパイルモード中の一時的なイミディエイトモードへの移行について、Forth ではワード [ および ] が担っています。F/L では、これらの記号は無名ワードブロックを構築するためのワードとして利用していますので、前後にアンダーバーを付け、_[ と ]_ というワードを新たに用意しました。
ダイナミックバインディングの採用
F/L では、ダイナミックバインディング(レイトバインディング)を採用しました。この問題は、言語システムの思想というよりも、どのようにプログラミングされるべきかにより決定される事柄なのだと感じました。
immediate モードのワードを Dict.object に集約した件
ワードはレイトバインディングされるため、コンパイル時には呼び出すべきワードが確定しません。そのため、必然的に Dict.object に集約することになります。最初はもうちょっと辞書毎にカテゴリ分けして実装できるのではないか、と根拠もなく思っていましたが、実装により本質が見えてくるという例として(というか、個人的な記録として)ここに記しておきます。
数値データに対する lit の廃止
リストを param ブロックとして使えるようにするため、数値が置かれていたら、それをデータスタックへプッシュするように内部インタプリタを変更しました。
コメント用ワードの変更
Forth では小括弧で囲まれた部分はコメントとして解釈されます。F/L でも当初はその方法を採用していましたが、小括弧はリストを表すために使用することとしました。一般的には C のコメント方法が広く知られていますので、F/L でもその方式を採用しました。
おわりに
コメントに使用するワードなんてどーでも良いじゃん、という気もしないではないですが、こんなところにまで、あーでもない、こーでもない、と考えたりしました。この辺は結構気楽な話なので、意外と楽しかった記憶があります。
F/L のような小規模な言語処理系においても、「どのような言語世界を作るのか?」ということを考えている場合、思想的な部分から、必然により導入された様々な機能、どーでもよい細かな部分まで、色々と考えることがあるのだなぁ、ということを感じていただければ幸いです。