Gforth と cross.fs
ForthFPGA
2022-8-10 14:11 JST

Gforth を使った組み込み用の Forth の cross 開発についてちょっと調べた。

@ の実装

Swapforth の j1a の@ の実装は nuc.fs 内で次のようになっている。2000 番地と or したアドレスにジャンプすることで、その値が取れるようになっている。

cross.fs
header @
@
    h# 2000 or execute
;

j1a の実装側は抜粋すると分かりづらくなってしまうが次のようになっている。pc は 1bit ずれているので、mem_addr[13] に 1 が立っているかどうかをチェックしているのと等価。

j1.v
    casez ({pc[12], insn[15:8]})
      9'b1_???_?????: st0N = insn;                    // literal

ついでにメモリ側も調整。mem_addr_w[13] に 1 が立っていて メモリへの write はありえないので、無視するように設計されている。

top.v
assign mem0_wr_w = mem_wr_w & !mem_addr_w[13];

この3つが微妙に絡み合っているのに加え、Python のプログラム(shell と読んでいる。UI のフロントエンド) の動き Tethered Mode の考慮があって、デバッグに手間取った。

蛇足ながら説明すると、Forth において @ はメモリ参照、! はメモリへの書き込み。

cross.fs 冒頭

Gforth で書かれている cross.fs の冒頭。variable で lst を宣言したその後、h# を定義している。Forth のマクロとも言うべき immediate とpostpone を使っている。immediate を使うと、入力された文字列を適宜変換できる。

マクロ(あるいはそれに準ずる機能)のあるインタプリタは便利だね。マクロというのは、ある特別なルールにしたがって入力された文字列を"即座に"(immediate に)変換して、変換したものに対して"後々"(postpone)実行をかけるものだ。

cross.fs
variable lst         .lst output file handle

: h#
    base @ >r 16 base !
    0. bl parse >number throw 2drop postpone literal
    r> base ! ; immediate

h# の使い方はh# 2000みたいな感じで 0x2000 をスタックに積むことができる。2000 を解釈するのは h# 内の記述(つまりマクロ的な記述=特別なルール)で 16 進数だと思いparse する。マクロ的な解釈が終わると postpone で literal としてスタックに積み上げる。

Forth が独特なのは入力を自分で一生懸命丁寧に位置文字ずつ解釈することかな。多くのインタプリタは自分自身で構文解析をしないよね。C で書かれた構文解析があって、その上でインタプリタとしてその言語が走る。Lisp の実装で、自分でミニLispもっていてそれが更に大きな Lisp を呼ぶというのもあるので、それと似たようなことをやっている言語はいくつもあるだろうけど、Forth はそこが徹底している。

Gforth と throw

Gforth は ANS Forth には(たぶん)ない throw というワードをサポートしている。とおもったあ Forth 2012 に throw exception の記述があった。

実際に h# を登録して動きを確認してみる。

cross.fs
$ gforth
Gforth 0.7.3, Copyright (C) 1995-2008 Free Software Foundation, Inc.
Gforth comes with ABSOLUTELY NO WARRANTY; for details type `license'
Type `bye' to exit
: h#  compiled
    base @ >r 16 base !  compiled
    0. bl parse >number throw 2drop postpone literal  compiled
    r> base ! ; immediate  ok
  ok
h# 8000  ok
.
:7: Stack underflow
>>>.<<<
Backtrace:
$7F66B7C72708 dup
$7F66B7C73E30 s>d

h# 8000とやっても何も積まれない。Stack underflow だ。これはコンパイル時に有効になるマクロだから。ワードの定義時に使うように設計されているということなので、ワードを定義してみる。

cross.fs
: my-word h# 8000 ;  ok
my-word  ok
. 32768  ok

:: で辞書を作成

なにをやっているか分かりづらいけど、::を定義しているコードを示す。これで ::で始まるワードの定義が予め設定されたメモリに領域に展開される。

cross.fs
wordlist constant target-wordlist
: add-order ( wid -- ) >r get-order r> swap 1+ set-order ;
: :: get-current >r target-wordlist set-current : r> set-current ;

例えばliteralの定義は次のようになっている。この辺は 16bit であるということが考慮されている。

cross.fs
: literal
    dup $8000 and if
        invert recurse
        ~T alu
    else
        $8000 or tcode,
    then
;

h#は次の通り

cross.fs
: base>number   ( caddr u base -- )
    base @ >r base !
    sign>number
    r> base !
    dup 0= if
        2drop drop literal
    else
        1 = swap c@ [char] . = and if
            drop dup literal 32 rshift literal
        else
            -1 abort" bad number"
        then
    then ;

:: h# bl parse 16 base>number ;

basewords.fs と nuc.fs

cross.fs でクロスコンパイル環境を作り、一部、すでに word を登録した。この次に basewords.fs で基本となる j1a のインストラクションをワードとして定義していく。

cross.fs
( 一部抜粋 )
:: noop      T                       alu ;
:: +         T+N                 d-1 alu ;
:: -         N-T                 d-1 alu ;

その後、nuc で核(nucleus)となる Forth のワードを定義していく。cross.fs でクロスコンパイル環境を作り、一部、すでに word を登録した。この次に basewords.fs で基本となる j1a のインストラクションをワードとして定義していく。

その後、nuc.fs ではheaderというワードでj1a用のワードを定義していく。なんでだろう?headerheader の方がちょっと便利になっているかな?

最後に:mainが定義されていて、ここで Forth の REPL に入る。うまく leds とかを nuc.fs で定義できなかったので(どたばたしていたので勘違いかもしれない)header がどうやっているのかを正確に調べないといけない。今は postpone。

あと、:mainに飛ぶ方法がわからない。途中でワードを増やしたりすると:mainのアドレスが変わるけど、自動で計算して 0 番地にそこに飛ぶコードを差し込んでいる(はず)。

Forth に慣れていないとなかなか完璧に読みこなすのは難しい。

おおまかな流れはわかったので、あとは自分で超 mini Forth + VM とか作ってみればいいんだね。過去に Gforth で mini VM は作ったことがある。もう忘れちゃったけど、Gforth は VM を作るテンプレートを持っているんだよね。

まずはこんなところ。あとは FPGA にどう活かしていくかだね。

リンク集