リターンスタック


Forthにおいてスタックといえば普通はデータスタックを意味する。整数値を受け渡すためのスタックである。しかし、forth環境は、これとは別個の、リターンスタックと呼ばれるスタックも持っている。実は、多少仕様は異なるものの、現代のPCでリターンスタックを使わずに動作しているものはほとんど無い。関数あるいはサブルーチンの呼び出しのとき、内部的にはリターンスタックの機構を使っているのである。しかし、普通の言語では、それはプログラム面からは隠されていて、直接にアクセスすることはできない。Forthにおいてもリターンスタックは内部で自動的に動作する機構であって、プログラマーが操作する必要のあるものではない。しかし、forthでは、値を臨時に一時格納する場所として、プログラマーがリターンスタックに直接アクセスすることを許しているのである。

このリターンスタックへのアクセス可能性を利用して、本来自動的に処理されるべき部分をプログラマーが撹乱することもまた可能になっている。それをすることが、forthのアクロバット的面白さであるといわれることさえある。

しかし、それは間違いである、ということで現在は大方の見解が一致している。今日では、リターンスタックの自動処理部分がどのように行われるかはforth環境毎に異なることを前提としなければならず、その部分の操作は、ある環境で上手くいったとしても、他でも動作する保証は何も無い。したがって、リターンスタックは、1ワード定義内に限ったデータの仮置き場としての使用法のみが許されている、と考えなければならない。

と、ひとくさり、説教くさい話をした後で、リターンスタックの"合法"な使用法とは、データスタックアイテムを一時的に取りのけておく、ということである。そのためなら変数を定義しておけば足りるともいえるが、いちいち名前をつける必要も無いような場合に、リターンスタックは簡便な方法を提供する。しかし、さらに考えると、これを利用する状況は、データスタックをそのままにするとまずい状況が生じているわけで、既に何か失敗しているのではないか、と疑うこともできるわけである。まあ、理想通りにばかりもいかないが。

>R R> R@

データスタックからリターンスタックにアイテムを移すワードが、>R である。
>R  ( S: x -- )  ( R: -- x )
R:と付いているコメントはリターンスタック上の効果である。S:はデータスタック上の効果を意味することは前にも触れた。
覚え方だが、forthでは"より大きい"記号> を"to"(〜へ)と読む場合がある。そこから、>Rはto Rで、リターンスタックへ、と読めばよいということになる。

リターンスタック上のアイテムを、データスタック上に取り出すのが、R> である。
R> ( S: -- x ) ( R: x -- )
これは目的地が普通のスタックなので省略されているということなわけだが、まあ、一方を覚えればその逆として覚えられる。

リターンスタックもスタックなのであるから、値は順に積み上げられていって、取り出すときには上に載っているものから、つまり、積んだときと逆順で出てくる。データスタックのアイテムは上から順に取られていくわけであるから、1つずつリターンスタックに移していくと、上下が逆に積まれていくことになる。しかし、移動の度毎に上下反転するのだから、データスタックからリターンスタックに移して、またデータスタックに戻してくれば、順番は逆の逆で、もとのままである。

R>はリターンスタックからトップの値を取り出してくるが、リターンスタックのトップの値を複製してデータスタックに置くワードがR@である。
R@  ( S: -- x ) ( R: x -- x ) 
リターンスタックの状態はもちろん変化しない。

リターンスタックの本来の機能と利用可能性

リターンスタックの本来の機能は、リターンアドレスを保存しておくことである。
それぞれのワード定義に対応する実行可能コードは、メモリーの別々の場所に格納されているため、あるワードから他のワードを呼び出したときには、コード実行は、現在実行中のコードの場所から一旦離れて、呼び出されたワードのコードが格納されている場所にジャンプしないといけない。しかし、呼び出されたワードに対応する処理が終わった後、もう一度、前にジャンプしてきたところの直後に戻って実行しないと、本来のワードの実行が完結しない。この、戻ってくる場所のアドレスがリターンアドレスである。ワードの呼び出しでジャンプするとき、その直後のメモリーアドレスが自動的にリターンスタックに格納される。そして、一連の処理が終わったときには、リターンスタックから値を抜き出し、その値の場所に戻るわけである。

上にも触れた「やってはいけない」技というのは、だいたい次のようなことをするのである。
: don't-do  R> R> swap >R >R ;
: word1  ( b -- )  if don't-do then ." one" cr ;
: word2 ( b -- )  word1 ." two" cr ;
0 word2  \ one two の順で印字される
-1 word2  \ two one の順で印字される
戻る地点を操作できるというわけである。
動作を少し説明すれば、word2が呼ばれ、そしてword1が呼ばれる。この時点で、リターンスタックのトップには、word2の中の"word1"の直後を指すアドレスが入っている。
word1のifがスタックをチェックして0ならばdon't-doは飛ばされて"one"と改行が印字される。ここでword1の内容は終わるので、リターンスタックからアドレスを取って戻ると、word2の残り、つまり、"two"と改行の印字が実行されて、全てが終了となり、入力待ち状態に戻る。
結果としては、one 改行 two 改行、と印字される。
word1のifにおいてスタック値が非ゼロなら、don't-doが呼び出される。この時点でリターンスタックには、上から順に "word1内のdon't-do直後"、"word2内のword1直後" の二つのアドレスが乗っている。そこで、don't-doはリターンスタックの上二つを入れ替える。すると、don't-doの内容が終了した時点では、リターンスタックは上から順に"word2内のword1直後"、"word1内のdon't-do直後"である。したがって、ここからリターンすると、word2に戻って"two"と改行が印字される。そして、word2の内容が終わった後には"word1内のdon't-do直後"がリターンスタックのトップにあるので、そこからリターンすると、word1のthenに至り、"one"と改行を印字する。ここで全ての内容は完了し、入力待ち状態に戻る。
結果としては、two 改行 one 改行、と印字される。

上のようなコードが動作することを保証しているのは、リターンスタックには、リターンアドレス以外のものが置かれていない、という状況である。システムは必要応じてリターンスタックを使用するが、現在のforthは、リターンアドレスだけを格納するわけではない。その都度、システムはリターンスタックの状態について想定を置くのであり、それが外れると操作がおかしくなる。したがって、リターンスタックの合法な利用法であっても、システムがリターンスタックを利用する際の想定を壊すような使い方をすると、予想外の動作となり、かなりの確率でシステムがクラッシュする。

というわけで、リターンスタックを使用する場合に注意すべき点は、次のようになる。
  1. リターンスタックが平坦なブロック内で>RとR>の個数が一致していなければならない。
  2. リターンスタックが平坦なブロックがいくつかある場合、別々のブロック間でリターンスタック上の値をやり取り、参照することはできない。
"リターンスタックが平坦なブロック"というのは、1つのワード定義内、および、DO-LOOPのループ内、FOR-NEXTのループ内である。つまり、指標値 I が利用できるループは、固有のブロックを形成する。BEGINで始まるループは固有のブロックは形成しない。入れ子のブロックは、段重ねに積み上げられると考えればよい。(ループについては後のページで説明する。)
上の基準から:
  • >RとR>は、ひとつのワードの定義内で実行回数が均衡していなければならない。
  • あるワード定義内から別のワードを呼び出しているときには、それぞれが別のブロックになるので、一方でリターンスタックに格納した値を、他方で参照したり取り出したりはできない。
  • DO-ループも固有のブロックであるから、DO-ループ内でリターンスタックに格納した値は、そのループ内で取り出し使用しなければならず、ループ内で>RとR>の実行回数は均衡しなければならない。
    • ワード定義でもループでも、それを途中で脱出する場合、それまでにリターンスタックに値を格納していたならば、脱出前にすべて取り出しておかなければならない。
    • ループでは、指標値 I や J を利用する場合、その直前にはリターンスタックはクリアーにしておかなければならない。
  • DO-ループの外で>Rした値をDO-ループ内でR@やR>によって参照、取り出しすることはできず、DO-ループ内で>Rした値を、そのループの外で参照、取り出しすることはできない。
    • DO-ループが入れ子になっている場合は、内側のループと外側のループとの間に、ワード定義とその中のDO-ループとの間と同じ関係が成り立つ。
  • DO-ループが始まる前に>Rした値は、そのループから抜けた後の部分ならば利用することができる。
    • しかし、DO-ループ外で>Rを用いた状態では、それをDO-ループ内で取り出すことはできないので、DO-ループ内からワード自体を脱出することはできない。

と、条件として書くと、かなりややこしく多くの制限があるわけである。しかし、現実には特に厳しい制限ではなく、リターンスタックは比較的よく使われる。データを格納しておく場所というより、データスタックからちょっと脇にそらす経路、と考えた方がよいと思う。
Mopsの場合、指標値 I を利用する場合にはリターンスタックに何か格納されてあってもかまわない。
しかし、Mopsでは、ループ内で頻繁にアクセスする値なら、リターンスタックを用いるより局所変数を用いる方がずっと効率が良いし、
脱出可能性の問題も生じない。

2>R 2R> 2R@

ふたつの項目を塊として扱ってリターンスタックを利用するためのワードも定義されている。
2>R は、データスタックのふたつの項目をリターンスタックに移動するが、その際、アイテムの順序はそのまま、つまり、データスタックが1,2の順なら、リターンスタックにも1 ,2の順で移動する。
2>R  ( S: x1 x2 -- )  ( R: -- x1 x2 )
この逆移動が2R> である。
2R>  ( S: -- x1 x2 )  ( R: x1 x2 -- )
また、リターンスタックに積まれたふたつの項目を、同じように順序を保ってデータスタックに複製するのが、2R@である。
2R@  ( S: -- x1 x2 )  ( R: x1 x2 -- x1 x2 )
ダブルセルで表現された数値か、文字列のaddr lenを扱うときには有用であるかも知れないが、forth標準ではどういうわけか以前からCORE Extensionワードとなっており、オプションではない。

【補】コントロール フロー スタック


Forth標準規格には、もう1つ、コントロール フロー スタックというスタックが出てくる。これは普通はプログラマーが明示的に利用するものではないが、操作可能にしてある環境もあり、そのための規格ワードも、オプションとしてではあるが、規定されている。そのことによって、実質的には、実行フローを制御するワードの実装方法も一定程度規格化する結果になっている。ここでは、操作の方法やワードについては説明はせず、どのようなものであるのか、簡単に説明しておく。
コントロール フロー スタック (CFスタック) は、独立のスタックであることもあるが、データスタックを用いて実装しても良いものとされている。コードをコンパイルするときに用いるものであって、解釈実行時には用いられない。その使用法のうち最も重要なのは、IF-分岐やループのときに、ジャンプ位置のコードをコンパイルしたり、ループが折り返して戻る地点を確定したりするために、主要地点のコードアドレスを保存することである。
例えば、IF-ELSE-THENの実装例で考えてみると、まず、IFのところには「スタック値が0であるときには次のELSEまでジャンプする」という内容の条件分岐コードをコンパイルする必要がある。そこで、0ならばジャンプというコードを、ジャンプ先は空白のままにしてコンパイルし、現在の地点のコードアドレスをCFスタックに置く。そして、条件成就の場合のコードをコンパイルした後、ELSEに至ったとき、コンパイルすべきコードは2つある。まず、ここまでコードを実行してきたフローをTHENまで飛ばすコード、そして、IFからジャンプしてきたフローの目的地を、THENへのジャンプコードの直後の地点に置くことである。そこで、THENへのジャンプコード(今度は無条件)を、またジャンプ先を空白にしたままコンパイルして、現在の地点のコードアドレスを取る。そして、CFスタック上のIFからのジャンプコードのアドレスを用いて、着地点を計算し、さらにもう一度そのアドレスを用いて、未定だったジャンプ先の値をコンパイルする。そしてCFスタックの値を、ELSEからのジャンプのコードアドレスに取り替える。そして、THENに至ったときは、またCFスタックの値を2度用いて、THENの地点までの跳躍量を計算した上で空白だったELSEからのジャンプ先の値を埋めるのである。すこし考えれば分るが、このやり方をすれば、途中にELSEがなくても、THENは正しい内容のコードをコンパイルする。(ForthのIF文の特性については、IF、EXIT、CASEの頁を参照してください。)

その他には、コロンとセミコロンなど、対で用いるワードについて、きちんと対応しているかどうかを確認するためのシステムフラグも、CFスタックに置かれることになっている。

浮動小数点数スタック(FPスタック)


伝統的forthでは、小数計算も整数を使って行っていたのであるが、小数、いわゆる浮動小数点数(floating point number)計算専用の演算装置(FPU)なども完備してきて、浮動小数点数も使うようになったわけである。その際、浮動小数点数スタックという概念が導入された。

「概念が」といったのは、実は、物理的には小数用の特別なスタックは配置せず、整数用の普通のデータスタックを使うことも許されており、現実にそのような環境も存在する。しかし、ここでは、物理的に別個の浮動小数点数スタックをもつことを前提として述べる。

浮動小数点数は、整数とは数値の表現形式が異なり、2進数の数量ではなくて、一種の記号として記述されている。大抵は、初めの1ビットが負号の有無に対応し、その後の何ビットかが小数点の位置の桁(指数部分)を表す数値、残りのビットが実質内容となる数量(有効数字)を表すようになっている。また、1セルのビット数が整数と異なる場合もある。実際、多くの32ビットforth環境は、浮動小数点数については64ビットを1セルにしている。

このように、forthにおいては、ほとんど唯一、数値のタイプの違いを明瞭に識別する必要があるのが、整数と浮動小数点数である。上のような1セル幅の違いに加え、FPUを持つマシンでは浮動小数点数計算にはレジスタまで専用のものが用意されているのが通常なので、スタックも物理的にキッチリとわけた方が扱いやすいのである。とはいえ、変数等に関していえば、1セルのビット幅さえ適合するなら、整数/小数のタイプを云々する意義は乏しい。

これまでの話からすれば、forthのスタックは、データスタック、リターンスタック、浮動小数点数スタックの3つのスタックを基本とする、といえることになる。forth言語システムの構造としては、これに辞書(ディクショナリ)と、インタープリタ、コンパイラを加えて構成される、と考えることになる。それがforthバーチャルマシンであるが、実際のハードウエアでは、インタープリタとコンパイラは、ワードとして辞書の中に登録されて、上記の全てはメモリーに配備され、辞書内のコードの実行に応じて演算装置が作動するわけである。

ただし、小数の操作や計算、浮動小数点数関数などに当たるワードは、forthの規格ではオプションとされている。したがって、環境によっては、装備されていない場合もある。また、浮動小数点数が利用可能な環境であっても、小数オプションファイルをロードしないうちは、浮動小数点数スタックも設定されないものもあるかも知れない。
PowerMopsまでは、浮動小数点数スタックは現実には存在しているが、floating pointライブラリーソースコードをロードしたとき初めて表示に現れる。
iMopsでは、始めから浮動小数点数ライブラリもロードしてある(ただし、ダブルセル整数を前提とするものは定義していない)。Mac OSXがCocoaベースになって以来、
システムコールに頻繁に小数が必要とされるからである。ダブルセル整数を省いたのは、64ビット環境では必要性がほとんど無いと思われたからである。
(実際には、32ビット環境でも、ほとんど必要はない。16ビットシステム以前の遺物というべきではなかろうか。もっとも、forthの得意分野である組込みマシンでは、
まだ必要かもしれない。しかし、PC環境では、普通のプログラムでそれほど大きな数が必要になる局面はほとんどなく、逆に本当に多くの桁数が必要な
科学計算のようなものなら、ダブル程度では足りないだろう。)
数値が小数であることを示すには、小数点をつければよい。小数ライブラリがロードされていると仮定すると、
3.1415
などとすれば、この値は整数スタックではなく、小数スタックに積み込まれる。

いわゆる Scientific notation(科学記法)もサポートされている。例えば、
1.02e-2
6.7e3
のような表記法である。eは10の何乗かということであり、その後の数字が10の肩の上に乗って、前の数と掛け算をするのである。e-2とは100分の1倍であり、e3は1000倍である。したがって、
1.02e-2 = 0.0102
6.7e3 = 6700
ということになる。eの後に何も書かないと、0乗つまり、1倍と解釈される。しかし、その場合、結果が整数であっても、小数としての整数と解釈されて、小数スタックに積み込まれる。したがって、小数スタックに1を置きたいときには、
1e
と最後にeを付ければ良い。もちろん、小数点(ピリオド)でもよいが。


浮動小数点計算はforthではオプションではあるが、実際のアプリケーションで小数計算をする必要があることも多いと思われるので、基本的な部分を説明したいと思う。しかし、浮動小数点数計算用のワードライブラリは、内容がかなりあるので、ここでは特別なスタックがあるという話にとどめ、ページを改めて、小数データ処理用のワードについて、整数に準ずる形で説明を試みようと思う。


次は、浮動小数点数用の変数と定数について説明する。


最終更新:2013年11月05日 10:30