はじめに ― デバッグとは“対話”である
FORTHにおけるデバッグとは、外部ツールを用いることではなく、言語自身に質問する行為 である。C言語やPythonのようにデバッガを起動してステップ実行するのではなく、gforthでは「実行系がすべて露出している」ため、コンパイル状態・辞書・スタックの中身などを、即座にその場で観察できる。
FORTHの哲学では、「プログラム=ワードの集積」であり、「デバッグ=ワードの分解観察」である。したがって、gforthでは“止めて覗く”よりも、“動かしながら覗く”ことが重要になる。
スタック観察 ― .S の基本
デバッグの第一歩は、スタックの状態を知ること である。gforthでは、.S(ドット・エス)を入力するだけで、現在のスタックの内容が表示される。
1 2 3 .S
<3> 1 2 3
ここで <3> は、スタックに3つの項目があることを示している。FORTHでは、ワードの入出力をスタック図で表す習慣があるため、.S はその確認に必須である。
スタック図の読み方再確認
DUP ( n -- n n )
DROP ( n -- )
+ ( n1 n2 -- n3 )
上記のように、「入力 → 出力」の順でスタックの変化を記す。.S はこの図を実行後の確認として照らし合わせる役割を果たす。
定義内容の表示 ― SEE ワード
次に、定義済みワードの中身を覗く方法を学ぶ。gforthには SEE というワードがあり、コンパイル済みのワードの内容(内部命令列)を表示してくれる。
: SQUARE ( n -- n^2 ) DUP * ;
SEE SQUARE
: SQUARE
DUP *
;
このように、SEE はワードが colon 定義(: ... ;)である場合、実際の展開後の形を人間が読める形で出力する。CREATE や VARIABLE で定義されたワードも、その構造に応じて内容を表示する。
: SQUARE ( n -- n^2 ) DUP * ;
: SQUARE ( n -- n^2 ) DUP DUP * * ;
SEE SQUARE
このように再定義しても、辞書には新しい定義が追加され、古い定義は参照されなくなる。SEE は常に「最新の定義」を表示する。
SEE IF
SEE LOOP
これらを実行してみると、制御構造も単なるワードとして辞書に登録されていることが分かる。この“可視化されたコンパイラ”という構造が、FORTHを単なるプログラミング言語ではなく、言語実験のための実験装置 として際立たせている。
組込みワード(ネイティブコード)に対して SEE を実行した場合は、機械語レベルまたは内部ラベルの一覧として表示される。SEE はコンパイラが生成したスレッド化コードを解析しているため、他のForth実装とは若干異なる出力になることがある。
辞書の探索 ― WORDS と FIND-NAME
FORTHの辞書は、「ワード名」「リンク」「実行アドレス」のリストで構成されている。この構造を覗く代表的なワードが WORDS である。
WORDS
DROP DUP SWAP OVER ROT ... SQUARE ...
これは辞書に登録されているワードの一覧を、最新定義から順に表示している(つまり、再定義されたワードは常に前に現れる)
FORTHの辞書は、各ワードの「名前フィールド(Name Field)」を鎖のように連結した構造になっている。gforthでは、この辞書を直接探索するために FIND-NAME という低レベルワードが用意されている。
s" DUP" find-name .s
<1> 7FFFB420E1C8
見つかった場合は、そのワードの「名前トークン(nt)」がスタックに残る。見つからなければ 0 が返される。
このntは、ワードのメタ情報を参照するためのハンドルであり、ここから実行トークンや名前文字列を取り出すことができる。
名前トークンから実行トークンを得る
NAME>INTを使うことで、名前トークンから実行トークン(xt)を取得できる。
s" DUP" FIND-NAME NAME>INT .S
<1> 7FFFB420E120
このxtは、' DUPで得られるものと同一である。つまり、FIND-NAMEとNAME>INTを組み合わせれば、'と同様にワードを動的に取得できる。
名前を文字列として取得する
辞書内の名前を再び文字列として確認するには、NAME>STRINGを用いる。
s" DUP" FIND-NAME NAME>STRING TYPE
DUP
これにより、辞書から取得したトークンが確かに指定したワードであることを確認できる。NAME>STRINGは( nt -- c-addr u )を返すため、TYPEで直接表示できる。
実行モードとコンパイルモード ― STATE の確認
gforthには二つのモードがある:
- インタプリタモード:その場でワードを実行する。
- コンパイルモード:新しいワード定義の中で命令を辞書に記録する。
STATEは、どちらの状態にあるかを確認することが出来る。
: .STATE ( -- )
STATE @ IF
." (Compiling...) "
ELSE
." (Interpreting...) "
THEN
;
\ このワードがコンパイル中にも実行されるよう IMMEDIATE に設定
IMMEDIATE
.STATEは、インタプリタモードとコンパイルモードでは違う動作を行う。
.STATE
(Interpreting...)
以下のようなワード定義を行うと、
: TEST .STATE 10 20 + ;
コンパイルされる時、(Compiling...)が表示され、「いまコンパイル中かどうか」をプログラム内で動的に判断できる。
デバッグの実践例 ― 動作を観察しながら修正する
以下は、意図的にバグを含んだ単純なプログラムの例である。
: SUM3 ( a b c -- sum )
+ ;
これを 3 4 5 SUM3 . と実行しても、正しい答えが得られない。原因は、3つの引数に対して加算が1回しか行われていないことだ。
3 4 5 SUM3 .S
でスタックを確認すれば <2> 3 9 が表示され、加算が1回しか行われなかったことが分かる。
修正版:
: SUM3 ( a b c -- sum ) + + ;
このように、スタック操作を逐次観察しながら修正していくのがFORTH流のデバッグである。
LOCATE ― ソースコード中の定義箇所を特定
LOCATE name
name がどのファイルのどの行で定義されているかを表示する。gforthはソースファイルを読み込む際、内部的に「定義位置情報」を保持しているため、LOCATE によって定義元のファイル名と行番号を調べることができる。
私が使用しているSUMというワードで実行してみた。
LOCATE SUM
~/.config/gforthrc:0
: sum
begin depth 1 > while
+
repeat ;
: fsum
begin fdepth 1 > while
f+
repeat ;
: clear
begin depth 0> while
自分で定義したワードに対しても、ソースファイルを INCLUDE 経由で読み込んでいれば、正しい位置情報が出力される。対話的に定義したワード(REPL上で入力したもの)は、一般に「no file information」として扱われる。
WHERE ― ワードが存在する場所を一覧表示
WHERE name
name が複数の辞書(語彙空間)に存在する場合、そのすべての定義位置を一覧表示する。特に、同名ワードが異なるモジュールで再定義されているときに有用である。
WHERE TYPE
kernel/io.fs:139:13: newline type 0 out ! ; 0
kernel/io.fs:255:45: swap 0 max 0 ?DO delta-I &80 min 2dup type +LOOP drop ; 1
kernel/nio.fs:103:19: over - spaces type ; 2
....
このように、複数のソースで使われている場合、全ての使用場所を一覧として示す。