Arkam
(Forthについてある程度知識がある人向けへの紹介です)
多々あるForthの実装の中には、シンプルなStackVMの上にForthシステムを乗せるタイプのものがいくつかあります。
その中でも特に以下のものに影響を受けて、自分でも作っています。
- uxn Orca作者の8bitなStackVM(Forthではない)。エコシステムの構築を目標にしているみたいで、uxn上で動くツールがどれもクールなので見てて楽しい
- retro (githubミラー) byte単位のアクセスが無いなど割り切った仕様。多くの言語・プラットフォーム上でVMが実装されている
- Mako Java上に実装されている。いわゆるFantasyConsoleな仕様で、たくさんのゲームが作者によって書かれている
用途は
- グラフィック・サウンド関連の小さいプログラム書いて遊ぶ
- Forthの特性を活かして様々な実験的コードを書く(下記)
- 簡単なツールを書く
などなどです。
ずっと個人的なプロジェクトに留めたいので使用者向けドキュメントなどを書く予定はありません。その代わり、自分で処理系作りたい人向けに内部の解説などを追加していこうと考えています。
仕様
32bitリトルエンディアン固定のVM(arkvm)の上に、それ自身で書かれたForth(arkam)が乗っています。
VMの命令は30個程度で、IO命令による拡張を前提としています。
Arkamの特徴としては、Retroの様にquotation(無名ワード)を多用しています。そのためWHILE系のループはなく、末尾再帰用のAGAIN
というワードだけでループ系のワードを構築しています。もちろんForthなので伝統的なWHILEなどを定義することもできます。
SDL2を使用したグラフィック・サウンドで遊ぶ用環境(sarkvm/sarkam)もあり、現在スプライトや内臓のFMシンセで遊ぶことができます。
Forthの面白いところ
以下は多少Forthを知っている人向けの、更に面白い部分への招待です。
メタコンパイラ
最初はMakoと同じく、Forth風の言語からVM命令へのコンパイラ(Sol)をCで書いていました。イメージの書き出しを容易にするためです。
しかし、Lispでマクロ脳を開発されたのもあり、同じような記述を何度も書くのが面倒になったのでSolでForthを書きました。
すると今度はForthの動作を変更するためにSolを書いたりSolのコンパイラを保守するのが面倒になったので、Forth自身でForthを書きました。いわゆるセルフホスティングで、Forth界隈の用語だとmetacompilerです。これが一つ目の面白いところです。
C言語などの普通のセルフホスティングは、ソースコードをパースしてオブジェクトコードを吐き出す、とごく普通のコンパイルプロセスを使います。
GCなどランタイムが必要な場合は、その言語のサブセットをC等に変換するものがあります。Scheme48のpreschemeや、SqueakのSlangなどです。どちらにしろ、パースしてコンパイル、という意味では同じです。
別のやり方として面白いものではmaruというlisp処理系があります。GC含めた処理系自身を自分でコンパイル(IA32の機械語書き出し)できるのですが、LispなのですでにパースされたS式を環境から取り出してそのまま使っています。
さてForthですが、arkamのメタコンパイラやChicken Schemeの作者のForth処理系(ミラー)などでは、さらに変わったやりかたとしてワードの動作そのものをコンパイラにしてしまいます。
普通、下記のコードではスタックトップに1を足すfoo
というワードを定義します。
: foo 1 + ;
メタコンパイラではワードの定義を開始する:
を再定義してしまい、ターゲット上のfooワードの定義を開始して、かつ書き出し用のfooアドレスを書き出し用領域にコンパイルするfooというワードを定義する、という二重の動作を持つようにします。
Lispだと、以下のような雰囲気です。
(define-macro (define name ...) 自身がコンパイラのnameというマクロを作る)
(define (foo n) (+ 1))
;; 1. ターゲット上の環境にfooを定義する
;; 2. ターゲット上のfooの関数呼び出しをターゲット領域にコンパイルするようなfooというマクロを現環境に作る
(define (bar)
(foo 1)) ; 実際にはfooマクロが動いて、定義時にbarの内容としてコンパイルする
なかなか文字での説明は難しいですね。
これによって何が嬉しいかというと、Immediateワードはmetaで定義するなど多少の制限はあるものの、ほぼ普通のForthコードをそのままコンパイルすることができます。また、パース部を書く必要が無いため、メタコンパイラも便利マクロも併せて数百行に収まります。
Forthで書かれたForthですが、ほぼForthの機能をそのまま利用できています。
リターンスタック
Forthで一番好きなところです。あんまりいじるな、という人もいますが、特にEarly Returnがとても楽になるので僕は大好きです。Forthの自然な多値の扱いと相まってコードの見通しがかなりすっきりします。
Arkamでは、例えば以下のコードを
: foo ( n -- ? ) # nが0,3,5ならyes、それ以外でnoを返す
dup 0 = IF drop yes RET THEN
dup 3 = IF drop yes RET THEN
dup 5 = IF drop yes RET THEN
drop no
;
次のように書くことができます。
: foo ( n -- ? )
0 [ yes ] ;case
3 [ yes ] ;case
5 [ yes ] ;case
drop no
;
この;case
は( a b q -- ... | a )
というスタックエフェクトで、aとbが=
ならqを実行して呼び出し元を抜けて、そうでなければaを残したままにします。
構文といえるような動作ですが、実際にはリターンスタックをいじる単なるワードです。
: ;case ( a b q -- ... | a )
>r over = IF drop r> rdrop >r RET THEN rdrop
;
rdropで呼び出し元を捨てています。
セルフホスティングコンパイラの中でもこういったEarly Returnは多用しています。
自然と短いワードを定義することになるので、読みやすさの向上にも貢献しているのではないでしょうか。
また、リターンスタックを操作できるということは継続そのものなので、Scheme等と同じように面白いこともできます。
簡単なCo-operativeなマルチタスクを実装していますが、処理系に手を加えたりASTを操作する必要はなく、Forth自身で100行程度で書かれています。
サンプルとしてのテストコードを抜粋します。$inc
というメッセージでカウンタを加算、$sum
でsenderにカウントを送るタスクですが、割と短く直感的に書けてるかなと思います。
mes: $inc
mes: $sum
COVER
var: count ( counterタスク以外からは見えない )
SHOW
[
[ ( message )
$inc [ parcel count + count! ] ;case
$sum [ count $sum sender SEND ] ;case
] recv # メッセージを受信し続けるループ。Erlangのよくあるプロセスみたいな使い方
] task: counter
END
他にもBohemGC風のConservativeGCやローカル変数もForth自身で書いています。
まとめ
その読みにくさから簡単なチュートリアルで触って終わったり、いわゆるconcatanativeな言語のたたき台にされがちな伝統的Forthですが、その実装のシンプルさ故にまだまだ未知の領域が広がってると思います。
複雑な概念や実装を扱うのも大切な領域があるとは思いますが、逆に極めてシンプルにするにはどうすればいいのか、について探求するのも楽しいものです。例えばFactorあたりからのquotationの逆輸入はシンプルさと読みやすさを大幅に向上させたと思います。
そういった嗜好の人が少しでも影響を受け、Forthに取り組み、新しい発見をして共有してもらえると大変助かります。よろしくお願いします。