EVALUATE


EVALUATEは「評価」である。要するに遅延評価である。ソースコード文字列を実行時にインタープリトするのである。
EVALUATE ( i×x c-addr u -- j×x )
解釈されるソースコードが入力を必要とすれば、それはスタックの、文字列より下の位置に置かれなければならない。また、返される値は、実行後にスタックに残される。
ソースインプットの長さに特に制限はないようだが、通常は、ごく短い(一行)コード断片が予定されている。

このワードは伝統的にインライン定義に利用されてきた。例えば、
: displace S" dup @ +" evaluate  ; immediate
のようにすれば、このdisplaceはimmediateであるから、コンパイル状態でも実行される。そのとき、"dup @ +"というコードが評価されるわけであるが、コンパイル状態であるので、このソースはその場にコンパイルされるのである。つまり、あるワードの定義がdisplaceを呼び出すと、その場に"dup @ +"と書くのと全く同じコードがコンパイルされることになるわけである。

EVALUATEは、ソースコードを実行時点で評価するので、入力ソースコード文字列は固定されたコード文字列である必要はなく、実行時までに何らかの方法でソースコード文字列を生成し文字列パラメターとして渡すだけでよい。また、コンパイル時点ではまだ定義されていないワードを含んでいても、実行時までに定義されていれば問題なく実行される。

ただし、EVALUATEは、ソース文字列内のワードを実行時にディクショナリ検索して実行することになるので、ランタイムに多用すると負担が大きく、相対的に実行速度が遅くなる。また、コード生成装置など、開発環境の存在を前提としているため、Mopsでは、単独実行可能アプリケーションで実行時にEVALUATEを用いることは原則禁止である。つまり、EVALUATEはコンパイル時に実行されてしまうのが望ましい。

PARSE


Parseはパースともパーズともいう。「構文解析」という意味である。つまり、このワードは構文解析をするのである。

とはいうものの、forthの構文解析というのはトリビアルである。このPARSEも大したことをするわけではない。このワードは、スタックからの入力値として、ASCII文字をひとつ取る。そして、その文字をインプットテキスト内でスキャンし、部分文字列をaddr len形式で返す。返される文字列はPARSEの直後(空白1つ空ける)から基準文字が最初に現れる位置の直前まで、である。
PARSE ( char "ccc<char>" -- c-addr u )
戻り値が指す文字列は入力バッファ(ソーステキスト読み込み用の一時使用メモリ)に格納されており、出力のアドレスは入力テキストストリームのバッファを直接に指す、ということになっている。実行後、入力の読み込みの完了地点のマークは、PARSEで検索され発見された文字の直後まで進められることになっている。PARSEのスタック効果コメントが入力ストリームから入力をとるという形式になっているのは、そのためである。結果として、PARSE以降のソースはそこまで読み飛ばされ、インタープリトやコンパイルに関しては無視されることになる。

このワードを用いれば、自ら構文解析を行うワードを定義できる。その意味で、局所的コンパイラ変形を可能にするワードである。内部的にも、文字列リテラルをデータディクショナリにコピーして実行時にaddr-len文字列を返すワードなどに利用できる(iMopsでは実際これを利用している)。
例えば、前も書いたことがあるが、IF-構文をそこだけ「普通の語順」に変えるなどということができる。次のようにするのである。
: if(  [char] ) PARSE evaluate postpone if ; immediate
: end-if  postpone then  ; immediate
これだけで、次のような構文が可能になる:
0 value flag-word

: if-test
 if( flag-word 0= )
   ." flag is zero." cr
 else
   ." flag is non-zero." cr
 end-if
;
もっと細かい構文解析をするコードを自分で書けば、一部、他言語のような構文を許すワードが定義できるだろう。このような構文変形の利点は、インタープリタやコンパイラ自体を変形するものではないので、他の部分は、まったく従来のままにできるという点である。もっとも、言語構成の変形は、目的と効果を十分に考えて行うべきであるし、十分なドキュメントも必要である(他のプログラマーのためだけでなく、将来の自分自身(絶対忘れる!)のためにも。)。

なお、区切り文字となるcharは1バイト数なので、残念ながら半角英数文字以外は利用できない。
Parseは構文解析であって、文ないし式、あるいは最低限フレーズ構造を分析するものである、という考えからは、全く奇妙な規格、ないしワード名である。
しかし、構文解析が、統語規則にしたがって記号列を分析するのは、そもそも、その文の意味を確定するためである。
Forthではワードが定義によって確定した意味をもち、それを取り出すことで、記号列の意味が確定される。
したがって、空白文字で区切られた各ワード名を示すだけで、構文解析は終わっている、と言ってかまわないのである。
したがって、これがforthの構文解析なのであって、他の複合的な構文規則を持つ言語(ほとんど全て!)について行われることだけが
「正しい」構文解析なわけではないのである。同じ"構文解析"でも、中身は、当然だが、構文規則のあり方に依存するのである。
とはいえ、PARSEもそれを用いて1ワードよりも長い文字列を取り出して処理するワードを構成するのでないと、利用価値は低いのではあるが。

WORD


PARSEと似ているが微妙に異なる動作をするのが、このWORDという名前のワードである。本来、入力ストリームからワードを析出するためのワードなのであろう。しかし、機能はもう少し一般化されており、スタック入力からASCII文字を取り、それを両端の区切り文字として間に挟まれた文字列を引き続く入力ストリームの中に特定して、そのアドレスと長さをカウンテド文字列の形式で返す。
WORD   ( char "<chars>ccc<char>" -- c-addr )
文字列は入力ストリームバッファ以外のバッファにコピーされた上で返される。カウンテド文字列に関しては下記参照。

このワードは性格上、空白文字を区切り文字に特定して利用する場合が多い。そのため、次の"ワード"に至るまで複数の空白文字があったとしても、それを自動的には読み飛ばさない。結果として、空白文字以外の文字をWORDの区切り文字に利用するときは、規格通りの動作をすると、やや予想外なことが起こることに注意しなければならない。
規格によれば、WORDは、まず入力中の区切り文字をスキップする。したがって、WORDの後に空白文字が二つ以上あると、二つ目の空白からは、区切り文字と比較されることになり、それが指定した区切り文字と異なるならば読み飛ばすことはできない。その結果、そこから文字列内容として返すための本体が始まってしまうのである。したがって、空白文字列以外の文字を区切り文字にした場合には、その文字列までの間の空白の個数に任意性がなく、ワードを区切るための一つきりでなければならない。

カウンテド文字列(counted string)というのはforthによる名称で、別名、パスカル文字列とも呼ばれる。文字列の先頭に、その長さのデータが格納されており、文字列の本体はその後に格納されているという形式である。この形式の文字列を、forthで一般的なaddr len文字列に変換するためのワードとして、COUNTが定義されている。
COUNT  ( c-addr -- c-addr u )
伝統的には、このカウンテド文字列の長さは1バイトまでに制限されており、したがって、一文字列は最大で英数255文字までに限られる。日本語文字が混じると、もっと短い。通常のワード名やちょっとした文字列ならこれだけあれば十分である。しかし、一般的に文字列を扱うとなると、現状のコンピューティングで考えると、これでは短か過ぎる。そこで、forth標準規格では、必ずしも長さは1バイトには制限されないものとして、環境に依存するもの、としている。とはいえ、Mopsでは理論上2ないし4ギガバイトまで可能な文字列クラスが利用されるので、カウンテド文字列に関しては、1バイト制限のままである。

PARSE-NAME


このワードは、機能的には、BL(ASCII#32)を区切り文字としたWORDと同等である。ただし、返す値は、addr len形式の文字列であり、規格によれば、入力ストリームバッファの位置を指すものとされている。
PARSE-NAME  ( "<spaces>ccc<space>" -- c-addr u )
実際上、入力バッファから「ワード」に当たる文字列を取り出すためには、PARSE-NAMEを用いることになるのであろう。

上のような奇妙な重複の理由はよくわからないが、憶測すれば、一種のレガシーであって、バラバラだった規格を統一しようとしてPARSE-NAMEという新語が導入された、ということではないかと思われる(調べたわけではないが)。

[  ]   STATE


左角括弧 [ は、コンパイル状態で用いられ、コンパイル状態を一時中断し、実行状態に切り替える。右角括弧 ] は逆に、コンパイル状態に切り替える。左角括弧は、その本性上Immediateワードである。
[   ( -- )
]  ( -- )
要するに、ワード定義を一時中断して処理を実行し、またワード定義に復帰する、という操作のためのワードである。典型として、次に説明するLITERALと組み合わせる使用法が紹介されている。

左角括弧によってコンパイルが中断されると、解釈実行状態に移り、そこに記載されたコードはコンパイルされることなく、ワード定義の外におけるのと同じように解釈実行される。
しかし、この中断状態の中で、新たなワード定義や変数宣言を入れ子にすることは禁止されている。

何のために導入されたものかは判然としないが、憶測すれば、当初はコンパイル時に既に確定している数値のコンパイルを効率化することを狙ったのではないかと思われる。しかし、現状、ネイティブコンパイラ方式の環境では、多くは、数値リテラルは機械命令の即値としてマシンコードにはめ込まれてしまうであろうから、この方法では効率化にはならない。

変数を呼び出すか、あるいは、何かの計算結果を返すワードを呼び出して、その数値を、やはり次のLITERALでコンパイルするという使用法であれば、もし値がそのコンパイル時に確定できるものである場合には、ワード自体を呼び出すよりも効率的にできるであろう。
他には、その場でマシンコードを直接にディクショナリにコンパイルしてしまう、という使用法もある。これはしかし、窮余の策の部類に属するであろう。
ワード定義の途中で、なにか状態整備のためのワードを実行するという状況も考えられなくはないが、どうしても途中でしなければならないということは滅多にない話であろう。

なお、これらの括弧は、ワード定義中でなければ動作しないというわけではなく、コンパイル状態と解釈実行状態を切り替えるという動作は、解釈実行状態でも有効である。したがって、
( 解釈実行状態 ) ]  ( コンパイル状態 ) [ ( 解釈実行状態 )
のように組み合わせれば、コンパイル状態と書いた領域は、コンパイルの状態になり、そこに何かのワード名を書けば、実行はされず、ディクショナリに、その呼び出しがコンパイルされる。もっとも、そのコンパイルされた呼び出しコードは何のワードにも属していないので宙に浮いてしまうが。
実践的に意味のある使用法ではない。

ありうる有効な使用法のひとつとして、コンパイルの条件分岐がある。
例えば、ソースコードをロードする際に、条件フラグを操作して、環境の違いに応じて別のコードをコンパイルしたいときがある。ワードの内容を丸ごと違うものにしたい場合には、解釈実行状態の地の部分に[IF]-[ELSE]-[THEN]を用いて異なる定義をかき分ければよい。しかし、一つのワード定義のうちコードの大部分は共通で、ごく一部だけを調整したい場合もある。その際には、[  ]が利用できる。
0 value aFlag
...
: Word3 (共通コード)
   [ aFlag ]
   [IF] (aFlagが真の場合のコード)  
   [ELSE] (aFlagが偽の場合のコード)
   [THEN]
  (共通コード) ;
のようにかき分ければ、[IF]-[ELSE]-[THEN]はどれもImmediateワードであるので、ロードの途中でaFlagをtrueまたはfalseに設定することで、別のコードをコンパイルすることができる。ただし、あまり頻繁にコンパイルを条件分岐すると、可読性が落ちるように思われる。

なお、コンパイル状態か解釈状態かは、STATEという変数が表示している。この変数の内容は、コンパイル状態であればTRUE、解釈実行状態ではFALSEとなっている。標準ForthではSTATEはVARIABLE変数であるから、内容を確かめるには、
STATE @
とする。MopsではVALUE変数であるから、@は不要である。

この値を操作すれば、コンパイラ/インタプリタの動作は、それぞれの指標にあった動作となるが、直接にこの変数の値をいじるべきではなく、上の[ ]を利用すべきこととされている。

LITERAL FLITERAL


これはコンパイラ変形というわけではないが、少し特殊なImmediateワードで、コンパイル状態においてのみ利用される。
数値をコンパイルするのに用いられる。
LITERAL   ( x -- )   run-time ( -- x )
このワードはIMMEDIATEワードであるからコンパイル状態でも実行されるが、そのときにスタックから数値を取る。そして、その数値をスタックに返すコードをコンパイルするのである。
例えば:
: aWord   [ 8 ] LITERAL  ;
aWord  \ -- 8
というように、aWordのコンパイルを中断してスタックに置いた数値8は、LITERALによってコンパイルされ、aWordを実行したときに、その値が返されるのである。しかし、これだけでは、
: aWord 8 ;
と同値であって、あまり意味がない。

意味があるのは、[  ]内で何かのワードを呼び出して、aWordのコンパイル時点での計算値や格納値を取り、それをLITERALでコンパイルする場合である。
: Word2  (コード)  [ (現在の状態に応じた計算) ] LITERAL (実行時の計算)  ;
角括弧とLITERALの組み合わせで達成されるのは、EVALUATEとは逆に、コードを先に評価してしまうことによる効率化であると解釈できるだろう。値が複数ある場合には、LITERALをその個数分、続けて書けばよいだけである。ただし、上から順にコンパイルしていくので、返される値は逆順になる。

FLITERALはLITERALの浮動小数点数版である。これも、IMMEDIATEワードであり、コンパイル時にFPスタック上にある値を取って、コードをコンパイルする。そのコードが実行されたときに、当の小数がFPスタックに置かれるのである。
FLITERAL ( F: r -- )    run-time ( F: -- r )
これも、コンパイル時に予め計算した結果を利用する場合に利用価値がある。

SLITERAL


オプショナルワードであるが、SLITERALというワードもある。String Literalであって、LITERALの文字列版である。
このワードもImmediateワードであって、コンパイル環境内でスタック上に置かれたaddr len文字列を取り、その内容をディクショナリーに保存した上で、実行時にはディクショナリー内の当該文字列のaddr lenをスタックに返すコードをコンパイルする。
SLITERAL  ( c-addr1 u -- )   コンパイルされたコードの実行時 ( -- c-addr2 u )
これが便利な局面は、正直、よくわからない。コンパイル時点の日時を文字列として取って、それを印字するワードを定義するような場合だろうか。
Mopsでは定義されていない。

もしforth風に定義するとすれば、次のようになるだろう。ただし、データアドレスが相対化されていないので、このままでは環境を再起動した後には再ロードしないと利用できないので注意。
: SLITERAL ( c-addr u -- )
   dup c, here postpone literal dup postpone literal  \ addr len をLiteralで保存。実行時にスタックへ。
   0 DO dup c@  c, 1+ LOOP drop  ;   IMMEDIATE \ 文字列を格納し、最後に残ったアドレスを落とす。IMMEDIATE指定。
先頭の" dup c, " はこのコード限りでは無意味であるが、文字列の保存形式として、先頭の1バイトに文字列のバイト長を保存する形式にしている。Forthの標準的な文字列保存形式にあわせた。Forthでは、プログラマーがそういった内部機構に依存したコードを書く可能性が否定できないので、そうするのが無難であると思う。もっとも、そうする必要は全くなく、先頭のdup c,は落としてもかまわない。その方式ならば、文字列長は、32ビット環境なら、2GBまで可能である。そこまでいうと無意味に大きいと感じられるだろうが、255バイトを超えられるのは利点である。

iMopsで実用可能な定義をすれば、内部機構依存になるが、次のようになる。
: SLITERAL ( c-addr u -- )  \  iMops
   dup c,   \ 念のため
   here rel-data> (literal) [abs-data>]  \ rel-data>はデータアドレス相対化。[abs-data>]は実行時絶対アドレス化。
   dup (literal)     \  (literal)はLITERALの非IMMEDIATE版
   0 DO dup c@ c, 1+ LOOP drop  ;   IMMEDIATE
要するに、アドレスを基準値を基に相対化し、実行時に、その時点での基準値を基に復元するのである。これがないと、ディクショナリーのメモリー位置を動かせない。近年は、バーチャルメモリーアドレスが用いられるので、実行可能ファイルは常に同じアドレスにロードするよう指定することができるが、Mopsでは、開発環境ではディクショナリーはヒープ(自由使用メモリー域)にあり、インストールしたアプリケーションでは実行可能ファイル内にあるので、相対化を適当に済ますわけにはいかない。


次は




タグ:

forth evaluate parse
最終更新:2020年10月24日 13:03