データスタックが変数の役割を果たすとはいえ、forthでも変数は定義できる。
けれども、構文上は、値はすべて一旦スタックにおかれ、スタックを通じて処理されるような形になる。

変数の種類としては、ワードとして定義するパブリックな変数と、1ワード定義内でだけ有効な局所変数がある。
Mopsにはこれらに加えて Local Section (ローカルセクション)という機構があり、近接する特定範囲の複数のワード定義に渡ってのみ存続する変数を定義することができる。

このページでは長くなり過ぎないようForthコアに属するものだけを説明し、オプションである局所変数とMops特有のローカルセクションはそれぞれ別ページに回すこととする。

パブリック変数(VARIABLE, VALUE)


VARIABLE


VARIABLE V1

と宣言することで、V1という名前の変数が定義される。これは普通のワードと同じ有効範囲をもつ。つまり、宣言がロードされた直後から、同じ名前のワードが重複定義されるまで、この変数は同一性を保ち、その名で呼び出される。普通に大域変数(グローバル)といっても良いのだが、序章でも説明したように、プログラムの全域で有効という意味での普通の大域変数とは微妙に異なる。というか、むしろ、変数もまたまさにワードであるということなのだが。

VARIABLEの宣言(定義)は、実行状態でなされなければならず、コンパイル状態(コロン定義内)では所期の効果は得られない。

このタイプの変数がforthでは最も古典的なものである。
VARIABLEは、変数として利用され、そのように理解されてはいるものの、実体としては変数というよりアレイ(配列)である。長さが1セル長(forth標準規格:1セルはCPUの標準メモリー幅—普通、8ビットコンピュータなら1バイト、16ビットなら2バイト、32ビットなら4バイト、64ビットなら8バイト)で固定されている配列、と考えても大過ない。第一、プログラム上名前が値とは同一視されない。名前と同一視されるのは、その配列オブジェクトのポインタである。そして、値を取り出したり、値を格納したりするために、それぞれ固有のワードを呼び出さなければならない。

値の受け渡しは、もちろん、スタックを経由して行う。

VARIABLEの値は、定義の時点で0に初期化されることになっている。全ビット0である。

VARIABLEから値を取るワードは @ 、アットマークである。単なる特殊文字のように見えるかも知れないが、forthには特殊文字というものは無く、1文字の名前を持つワードは数多くある。これもその一つである。フェッチ (fetch) と読む。

例えば、上の変数V1の値を取るときは、

V1 @

とするわけである。まず変数名、そして動作、である。変数名をコードに書いたとき、つまり、VARIABLEを呼び出したときにも、実は固有の動作がある。それは、その変数のために割り当てられたデータ領域(メモリ)のアドレス値をデータスタック上に置く、ということである。だから、V1と書いた時点で、V1に割り当てられたデータ領域のメモリーアドレス値がデータスタックに押し込まれているのである。そこに、@を作用させると、@の動作は、スタック上のメモリーアドレス値を消費し、それが指している場所から1セル分の値を取ってスタックに押し込む、ということになっている。結果として、上のコードの結果、V1に格納されていた値だけがスタックに残されるのである。

したがって、VARIABLE V1と@のスタック効果を書くと:

V1  ( -- addr )
@   ( addr -- x )

xが格納されていた値である。なお、V1に格納された値は、スタック上にコピーされるのであって、取り出されるとV1の中身がなくなってしまうのではない。念のため。

VARIABLE に値を格納するワードは ! 、びっくり(エクスクラメーション)マークである。ストア(store)と読む。値はもちろんスタック経由で取る。
例えば、- 7 を V1に格納(代入)するには、

-7 V1 !

と書く。日本語的には「-7 を V1 に代入」とそのまま読める。
! のスタック効果は、

!   ( x addr -- )

である。x はスタック上にある値なら何でも良い(タイプは制限ない)し、addr は、1セル分のメモリースペースのアドレスであれば何でも良く、いつもどれかのVARIABLE名を書かなければならないわけではない。この辺の抽象性から、いかにもforth風の展開が可能になっていくのである。

VALUE


VALUEもVARIABLEと同じように実行状態で宣言することで定義されるパブリック変数である。
データ領域の大きさも同じように1セルである。
ただし、宣言時から既に、VARIABLEとは挙動が異なる。というのは、VALUEの場合、宣言時にスタック上にある値を用いてデータ域が初期化されるのである。初期値とする値は、ロード中にプログラムが部分的に実行された結果の値でもよい。ともかく宣言時にスタックに値が一つ必要であり、それなしに宣言すると、スタックアンダーフローエラーが出てロードも中断されてしまう。

例えば、初期値-1のVALUE、VL1を定義するには、

-1 VALUE VL1

と書く。

VALUEでは、その名前と格納された値とが同一視できる。つまり、一般のプログラミング言語の変数のように見える。動作としては、名前を書くと、格納されている値のコピーを一つスタック上に置く。値取り出し用ワードは不要である。
スタック効果を書けば:

 VL1   ( -- x )

また、新しい値に替えるときは、プレフィクス (prefix) を使う。つまり、前置するワードである。こういうものはforthには珍しい。これがかなり後になって追加された機能であることを物語っている。標準のプレフィクスはTOである。値はスタックから取る。
例えば、VL1に37を代入したいときには、

37 TO VL1

と書く。

TOの同義語として、 -> が定義されている場合も多い。マイナスに不等号をつなげた、まあ、要するに矢印である。このときは、

37 -> VL1

と書ける。Mopsではむしろこちらが本体であって、直観的にも分かり易いので個人的にはこちらを愛用している。
Gazinta(ガジンタ?)という呼び名がある。初めは謎だったが、"goes into"のスランギッシュな言い方であろうと推測される。

TOないし->のスタック効果は、次のように表記する。

TO   ( x “<spaces>name” --  )

引用マークに囲まれている入力項は、これはデータスタックから取るのではなく入力テキストから取るのだ、ということを意味する。何を取るかというとVALUEの名前である。つまり、プレフィクスTOにとっては次にあるVALUE名VL1は必要な入力項目なのである。実際、TOや->の後に何も書かないとエラーが出る。後方に書くのに入力としてコメントの左辺に寄せるのは、今ひとつ直観的ではないが、そういう表記法が規則であるようだ。
<spaces>というのは、はじめの空白文字はいくつあっても飛ばして、空白文字ではない文字列をVALUEの名前として解釈するという意味である。
実は、TOは使用法が拡張されていて、それが受け付けるのはVALUEだけではない。そのため、forth規格書では数値xが複数のこともあり得るような書き方になっている。TOが受け付けないワード名の場合は、エラーがでて中止になる。

ただし、上のTOのスタック効果は、解釈実行時のものである。他のワードの定義(後述)内において利用された場合は、効果が異なる。それを書いてみると、
TO  コンパイル時: ( "<spaces>name" -- )   実行時: ( x -- )
となる。つまり、ワード定義内でも、TOや->の直後にはValue変数の名前が必要であり、その定義内には「実行時にスタックの値を取って、その変数に格納する」という内容のコードがコンパイルされるのである。TOないし->だけをコンパイルしようとするとエラーになる。

配列(Array)


配列としてのVARIABLE

1バイト配列

VARIABLEは1バイト配列として利用することができる。全体長は1セル分である。ここでは現状一番多いであろう32ビットシステムで考える(2019年現在CPUは64ビットが圧倒的に多くなったが、Forthシステムは32ビットに止まっているものがまだ多いように思われる。)。この場合、1セルは4バイトであるから、1バイトなら4項目配列になる。

メモリーアドレスの個所で、1バイト数を出し入れするワードは、Cを付け足したフェッチ/ストアがある。つまり、c@、c!である。

c@   ( c-addr -- uc )
c!   ( c c-addr -- )

使用法は、

7 8 9 10  V1 3 + c!   V1 2 + c!   V1 1 + c!   V1 c!

V1 3 + c@    \ 10
V1 2 + c@    \ 9
V1 1 + c@    \ 8
V1 c@       \ 7

"x +"という形で普通に数値を加算することで配列の項目が進められるわけである。

頭に付け足されるcはcharacterの頭文字である。1文字1バイトとは限らない現在では、やや場違いな命名に見えなくもない。

1バイト数値をスタックにコピーするとき、より一般には1セルより小さいバイト数の数値をコピーするときには、正負の符号をどうするかという問題が出てくる。上のc@は全てを正の値として取り出す(0~255の値になる)。コメントのucのuはunsigned(符号なし)の意味である。正負を反映したい場合は、c@Xを用いる(-127~126の値になる)。最後のXはextend(拡張)を意味し、符号のビットを拡張するということである。

c@X   ( c-addr --  c )

2バイト配列

VARIABLEは、2バイト、2項目配列としても利用できる。その場合の操作ワードは、Wを付け足した、w@, w!, w@Xである。
頭のwは、wordの頭文字だったようだが、ダブルバイトと考えても良いと思う。
もっとも、これらはforth標準のワードではない。いくつかの環境では定義されており、Mopsでも利用できる。

w@    ( w-addr -- uw )
w!    ( w w-addr -- )
w@X   ( w-addr -- w )


Forthでは、配列も、原則として、項目番号ではなくてアドレスのバイト数値で位置を特定するので、2バイト配列を1項目進めるには、2を足さなければならない。例えば:

-1 3 V1 w! V1 2 + w!

V1 w@      \  3
V1 2 + w@X   \  -1 
V1 2 + w@    \  65535

(数値の理由が分らない人はコンピュータ内の数値表現法の基礎を勉強しましょう。)

CREATE(クリエイト)

ForthにはCREATEというワードがある。これを用いた宣言により、VARIABLEと同じように、利用可能なデータ域のメモリーアドレスと結びつく変数ワードを作りだすことができる。ただし、メモリー内に領域を確保しない。そこで、宣言の後に、必要分の領域を確保するためのワードを実行することで、任意の長さのメモリー領域、基本的にはバイト配列、を作りだすことができる。

ALLOT

データ領域を確保する最も基本的なワードはALLOTで、確保すべきバイト長に当たる数値をスタックから入力として取る。

以上のような仕組みで、512バイト幅の配列theArrayを作るとすると、

CREATE theArray 512 ALLOT

とすればよい。定義された配列theArrayはパブリックワードとなる。

値の出し入れは、項目を何バイトにするか、負の数を必要とするか、に応じて、上で説明したフェッチ/ストア用ワードを使えば良い。
項目位置の計算は、いつもベタのアドレスのバイト数値で計算する。

ALLOTはデータ領域は確保するが、内容の初期化はしない。新規定義の場合はメモリー領域は0で初期化されているのが普通だが、メモリーは前に何かで使ったゴミが残っていて不規則な値になっている場合もあり得る。

RESERVE

内容を消去、つまり、0に初期化して確保するワードとして、MopsにはRESERVEがある。RESERVEという語はデータ域を確保するという意味で通用してはいるようだが、forth規格にはこのワードは見当たらない。RESERVEが定義されている場合は、

CREATE theArray2 256 RESERVE

とすれば、theArray2は、256バイト幅の配列として定義され、確実に0で初期化される。

CELLS

さらに、1セル単位の配列を作るときは、CELLSというワードが便利である。これは、スタック上の値をセル幅倍する。
例えば、64セルある配列theArray3を定義するには、

CREATE theArray3 64 CELLS ALLOT

とすればよい。見た目は理解し易いと思う。

2! , 2@

セルが2つ分のアレイ、あるいはダブルセル変数については、直接に値の出し入れができるワードがある。2@と2!である。これは、アイテム2つを一度にフェッチ/ストアする。
2@  ( addr -- x1 x2 )
2!   ( x1 x2 addr -- )
格納の順番は、スタックの上の方がデータ域の前のセル、下の方が後のセルに入る。取り出しは、値をスタックから格納したときの順序を保つ。
これらは、ダブルセル数値(例えば、32ビット環境なら64ビット数まで)の取り扱いに適するもののように思われるが、なぜかforth標準では、COREワードとして規定されており、オプショナルではない(forth'94からそうであり、forth200xでも維持されている)。文字列などにも使えるからであろうか。

基本はこんなところと思われる。次は、格納された値を変化させるワードについて、ページを変えて説明しようと思う。




最終更新:2019年03月19日 22:37