読者です 読者をやめる 読者になる 読者になる

レガシーコード生産ガイド

私に教えられることなら

Forthの仕組み6 - 再帰、リターンスタック、インタラクティブ入力

前回

今回は、インタラクティブなコードの入力、リターンスタックの操作と再帰を追加していく。

サンプルコード

組み込みワード追加

必要なワードと、デバッグに便利なワードをいくつか追加する。

latest, latest!

辞書で最後に定義したワードのアドレスを取得するlatest ( -- addr )と、設定するlatest! ( addr -- )を追加する。

スタックエフェクトの通り、latestは取得してスタックに置き、latest!はスタックトップのアドレスに設定する。

bye(C言語用)

C言語版はインタプリタを終了させるためのワードが必要なので、伝統的なForthに則ってbyeとして実装する。中身は単にexit()を呼び出している。

スタック表示

ワード.sで、スタックに積まれてる値の数と内容を表示できるようにする。これはスタックポインタ取得ワードなどを実装して、Forth自身で組んでもいい。(自作のForth処理系ではそうしている。)

入力に合わせて、以下の例のように、スタックトップが右に来るように表示する。

1 2 3 .s    ( 実行コード )
<3> 1 2 3   ( 出力       )

C版とJavaScript版、どちらもスタックを配列で作っていて、スタックポインタdspは単なる配列のindexなので、配列の長さを表示、ループして値を表示する。

リターンスタックの操作

リターンスタックを操作することで、いろいろと面白いことができる。リターンスタックについては、一番最初のForthの仕組み - ワード実行 - レガシーコード生産ガイドで説明している。

r>, >r

r> ( -- a )はリターンスタックから戻り先を一つポップして、データスタックに置く。>r ( a -- )はデータスタックから値を一つポップして、リターンスタックに置く。

JS版でもC版でも、dpop(),dpush()rpop(),rpush()を使うだけでいい。

exit

リターンスタック操作の使用例を二つ紹介する。

ワードの途中で処理を抜けるexit(retと同じ挙動)は、以下のように定義できる。

: exit r> drop ;

どう実行されるかを詳細に書いてみる。

: b  1 +  exit  2 + ;
: a  b 3 + . ;

このようにワードabを定義して、4 aと実行すると、8と表示される。bで1足されて、2足される前にexitで抜けてしまい、最後にaに戻り3足されて表示される。

bが呼ばれたときのリターンスタックのトップ、つまりbからのaへの戻り先をAexitが呼ばれたときのbへの戻り先をBとする。

: b  1 +  exit ( Bはここ )  2 + ;
: a  b  ( Aはここ )  3 + . ;

exitが実行されたとき、リターンスタックはA Bとなっている(右が上)。exitの内容で、r> dropによってリターンスタックのトップが捨てられ、Aだけが残る。

忘れがちだけど、ワード;retコンパイルされる。exitの最後のretによって、リターンスタックのトップの場所に戻る。

このとき、リターンスタックのトップはAなので、Aに戻る。よってB以降の残り(2 +)は実行されず、aの残り(3 + .)の実行に移る。

twice

もうひとつ、http://peace.2ch.net/test/read.cgi/tech/1073673931/56よりワードtwiceを引用して紹介する。(自分の書き込みではない)

: twice  r> dup >r >r ;

という定義で、ワード内でtwiceを実行した場合、以降の処理を2度行う。

: c  1 .  twice  2 . ;

上のようにワードcを定義して呼ぶと、1 2 2と表示される。

cでのtwiceの戻り先をCとする。

: c  1 .  twice  ( Cはここ )  2 . ;

twice実行時点で、リターンスタックのトップにはCが置いてある。twiceの中身r> dup >r >rによってリターンスタックのトップが複製され、C Cとなる。

次に、twiceの最後にコンパイルされているretによって、まず一度Cに戻る。このときリターンスタックにはCが1つ残っている。

Cから残りを実行していき、2 .のあと、cの最後にコンパイルされたretの実行になる。

ここでまだリターンスタックのトップにはCが残っているので、retによってもう一度Cに戻り、結果twice以降が2度実行されることになる。

再帰

Forthの仕組み - コンパイル - レガシーコード生産ガイドで書いたように、Forthでは定義中のワードと同名ワードの呼び出しは、再帰ではなくその前に定義された同名ワードの呼び出しとなる。

: foo 1 . ;
: foo foo ;
foo    ( => 1 )

再帰を行うためには、定義中のワードのCFAを知り、それを辞書にコンパイルする必要がある。それを行うワードrecurを作る。

ワード:でヘッダを作った時点でlatestはそれに更新されているので、定義中のワードのヘッダアドレスはワードlatestで取り出す事ができる。

あとはそれを>cfaでCFAに変換し、辞書に置けばいい。

また、これらはコンパイルモード中の話になる。なのでimmediate!でイミディエイトフラグを立てておく。

: recur latest >cfa push-dict ; immediate!

例として、スタックトップの数字を、0より大きい間カウントダウンしながら出力するワードをrecurを使って定義してみる。

: countdown  dup . 1 - dup 0 >  if recur then drop ;

10 countdown  ( => 10 9 8 7 6 5 4 3 2 1 )

インタラクティブ入力

Forthの強みの一つである、インタラクティブな開発のためのコンソールを作る。JavaScript版とC版ともに、interpret()関数にユーザからの入力を文字列として渡す仕組みを作る。

実装方法はそれぞれの言語・環境での話になるので省く。

伝統的なForthは、Return/Enterキー入力でokというプロンプトが同じ行に出る。また、ワード.などの出力も入力と同じ行に出る。

例えばgforthでは、次のような画面表示になる。(4okがプログラムからの表示、それ以外が入力)

: inc 1 + ;  ok
3 inc . 4  ok

今回はこの処理は行わない。C版とJavaScript版どちらも、okプロンプトは表示せず、改行入力でそのまま改行してからプログラムからの出力を行う。

おわりに

この後どう進むか考えていたけど、仕組みの解説はもう終わっていると判断したので、今回で一度終わることにする。

今までの記事を一覧してみる。

  1. Forthの仕組み - ワード実行 - レガシーコード生産ガイド
  2. Forthの仕組み - 解釈実行 - レガシーコード生産ガイド
  3. Forthの仕組み - コンパイル - レガシーコード生産ガイド
  4. Forthの仕組み - 制御構造if - レガシーコード生産ガイド
  5. Forthの仕組み5 - ループ - レガシーコード生産ガイド

記事を書くことで復習したかったこと、わかりやすく解説を書きたかったことは1-5までだった。

Forthは仕組みが非常に単純で、簡単に作れる割に、自己拡張でいろいろなことができて面白い。また得意不得意もはっきりしているので、プログラマが道具箱に加えておくと役に立つんじゃないかなと思う。

しかしそれをアピールできたかはあやしい。まあわかりやすい解説を書くこと、読みやすい文章を書くことも技術なので、それらをまた磨きたいと思ったら、その度に同じ部分の解説を書き直してもいいかもしれない。

ただ今は文章を書く技術より先に磨きたいことができたので、一旦休憩します。お疲れ様でした。

広告を非表示にする