低レベルテキスト解析ルーチン

戻る



 前回 QDECCH というルーチンを見ましたが、同じようなルーチンが他にもありますので、今回はそれらをまとめて見ていきたいと思います。

・QALPHA

 英字であるかどうかを調べます。変数名が現れる可能性があるとき、これで英字かどうか調べ、英字なら変数であると判断するのに使います。小文字はどうなるの?という疑問もあるかと思いますが、CMBでは入力した時点でダブルクオートで囲まれていない英小文字はすべて大文字に変換されます。
QALPHA:	MOV	#H'5B,R13	; 'Z' の次のアスキーコード
	CMP/HS	R13,R0		; 'Z' を超えているか?
	BT	CLRCY		; 超えていればTビットクリヤしてリターン
	MOV	#H'41,R13	; 'A' のアスキーコード 
	RTS			; CMP/HS の結果がそのまま返すべきTビットの値
	CMP/HS	R13,R0		; 'A' 以上か?(遅延スロット)


・SKIPSPグループ

 これまでにも何度か出てきたけれど解説していなかったルーチンです。空白以外の文字が現れるまでR3を進めます(実際にはアスキーコードが H'01〜H'20 の文字はすべて空白とみなしています)。入り口は LSKPSP と SKIPSP の2つあります。LSKPSP はEXEPをR3にロードしてから SKIPSP にそのままなだれこみます。
LSKPSP:	MOV	EXEP,R3		; R3にEXEPをロード
SKIPSP:	MOV	#H'20,R13	; 空白のアスキーコード

SKPSPL:	MOV.B	@R3,R0		; 一文字読む
	CMP/EQ	#0,R0		; 行末の '\0' か?
	BT/S	SKPSPE		; 行末なら終了
	EXTU.B	R0,R0		; 符号無し化
	CMP/HI	R13,R0		; 空白(または印刷不能文字)か?
	BT	SKPSPE		; 空白でなければ終了
	BRA	SKPSPL		; 空白なら繰り返し
	ADD	#1,R3		; R3を1バイト進める(遅延スロット)

SKPSPE:	RTS			; リターン
	NOP			; (遅延スロット)

 最初に、読んだ値が行末の '\0' であったらそれ以上R3を進めずにリターンします。つまり、 SKIPSP は行末を超えて進むことは決してありません。さらに H'20 と比べそれ以下なら空白とみなし、R3をインクリメントして処理を繰り返します。
 SKIPSPのもう一つ重要な効果は、最初に見つかった空白でない文字をR0に入れてリターンすることです。ほとんどの場合空白を飛ばした後、最初に見つかった空白でない文字はいったい何であるか、調べることになりますからそれがすでにR0にロードされているのは非常に便利です。

・QEOSグループ

 BASICの文の終わりかどうかを判断します。EOS は end of statement の意味です。入り口は LQEOS、SQEOS、QEOSの3つあります。SQEOS は SKIPSP で空白を飛ばしてから、LQEOS はR3にEXEPをロードしてから空白を飛ばして QEOS を実行します。
 ここで注意してほしいのは QEOS を直接呼ぶ場合はメモリからの文字の読みこみは行いません。SQEOS か LQEOS なら SKIPSP を呼んでいるのでR0には調べるべき文字が入っています。QEOS を直接呼ぶ場合はあらかじめR0に調べたい文字を入れておく必要があります(MOV @R3,R0 などの命令を BSR の遅延スロットに入れればいいからそれほど無駄な感じではないですが)。ちなみにCCMBでは SQEOS しか使っていません。CMBでは LQEOS が使われる局面もあったのですがいろいろ省いているうちに無くなりました。
LQEOS:	MOV	EXEP,R3		; EXEPをR3にロード
SQEOS:	STS.L	PR,@-R15	; SKIPSP 呼びだしのためPR保存
	BSR	SKIPSP		; SKIPSP 呼びだし
	NOP			; (遅延スロット)
				; SKIPSP 実行後はR0には目的の文字が入っている
	LDS.L	@R15+,PR	; PR復帰

QEOS:	CMP/EQ	#H'3A,R0	; ':' か?
	BT	QEOSE		; ':' ならTビットをセットしてリターン
	CMP/EQ	#H'27,R0	; シングルクオート(注釈)か?
	BT	QEOSE		; シングルクオートならTビットをセットしてリターン
	TST	R0,R0		; 行末か?
	BT	QEOSE		; 行末ならTビットをセットしてリターン
	RTS			; リターン
	CLRT			; Tビットをクリヤ(遅延スロット)

QEOSE:	RTS			; リターン
	SETT			; Tビットをセット(遅延スロット)

 見てのとおり、コロン(マルチステートメントの区切り文字)、注釈(BASICは注釈が現れたらそこから行末までは何も無いものとみなします)、行末の '\0' を検出したらTビットをセットしてリターンするルーチンです。なんに使うかというと、PRINT文などパラメータの個数が可変な文で、文の終わりなのか、それともまだ読み込むべきパラメータがあるのかを判断するために使います。

・EXPRPAグループ

 右括弧が存在することを期待するルーチンです。expect right paren の意味です。
 数式中に左括弧が出てきたら、いずれ必ずそれと対応する右括弧が出てくるはずです。このルーチンは右括弧が存在するべき場所で呼び出し、存在しなければSYNTAXエラーを発生させます。存在した場合何事も無かったように戻ってきます。
LXPRPA:	MOV	EXEP,R3		; (LQEOS、SQEOS と同じ)
SXPRPA:	STS.L	PR,@-R15
	BSR	SKIPSP
	NOP

	LDS.L	@R15+,PR

EXPRPA:	CMP/EQ	#H'29,R0	; ')' か?
	BF	JSYNERR2	; 違っていたら SYNTAX ERROR にする。戻ってこない。
	ADD	#1,R3		; ')' のぶんだけR3を進める
	RTS			; リターン
	MOV	R3,EXEP		; R3をEXEPに書き戻す

 R3が指している文字が右括弧ならそのぶんR3を進め、最後にR3の値をEXEPに書き戻します。右括弧でなかった場合 SYNTAX ERROR にジャンプし、戻ってきません。
 EXP△□グループのルーチンは、「それが存在するのが当然」という処理なので、Q△□グループと違い、結果をTビットで返すことはしません。Q△□グループは成否をTビットで返すのに対し、EXP△□グループは「失敗したら戻ってこない、成功したら何事もなかったかのように戻ってくる」ということで成否を返すわけです。これだと判定ルーチン呼びだし後に呼びだし元で条件分岐しなくてよいので、呼びだし側の処理は書きやすくなります。

 ところで、不一致時に飛ぶラベルが JSYNERR2 になっています。CCMBには JSYENRRx というラベルが全部で5個所にあります(CMBはもっと多いです)。これは、BT、BF、BT/S、BF/Sといった条件分岐命令が近くにしか飛べないからです。無条件分岐命令 BRA はもっと遠くに飛べるので、プログラムの各所に BRA SYNERR という命令を散りばめて置き(もちろん必要のないところには置きません)、条件分岐命令は最寄の BRA にジャンプすることでどこからでも SYNERR に飛べます。
 CCMBは BRA で全部届いているのですが、CMBになると JMP でないと届かないケースもあります。



・EXPCOMグループ

 expect comma の意味です。EXPRPA グループと同様、コンマがあるべき場所で呼びだします。内容は EXPRPA とほとんど同じなので説明は省略します。
LXPCOM:	MOV	EXEP,R3		; (LQEOS、SQEOS と同じ)
SXPCOM:	STS.L	PR,@-R15
	BSR	SKIPSP
	NOP

	LDS.L	@R15+,PR

EXPCOM:	CMP/EQ	#H'2C,R0	; ',' か?
	BF	JSYNERR2	; ',' でなければ SYNTAX ERROR にする。戻ってこない
	ADD	#1,R3		; ',' のぶん1バイトR3を進める
	RTS			; リターン
	MOV	R3,EXEP		; R3をEXEPに書き戻す(遅延スロット)


・EXPSTRグループ

 特定の文字列が存在することを期待します。存在しなければ SYNTAX ERROR になります。FOR文で "TO" や "STEP" があるべき場所で使っています。比較したい文字列の先頭番地をR2に入れて呼びだします。
LXPSTR:	MOV	EXEP,R3		; (LQEOS、SQEOS と同じ)
SXPSTR:	STS.L	PR,@-R15
	BSR	SKIPSP
	NOP

	LDS.L	@R15+,PR

EXPSTR:	MOV.B	@R2,R0		; R2が指す文字を読みこみ
	MOV.B	@R3+,R1		; R3が指す文字を読みこみ
	TST	R0,R0		; R2が指す文字が
	BT	EXPSTE		; '\0' なら比較完了
	CMP/EQ	R1,R0		; R2が指す文字とR3が指す文字を比較
	BF	JSYNERR2	; 違ったらエラー
	BRA	EXPSTR		; ループ先頭に戻る
	ADD	#1,R2		; R2をインクリメント(遅延スロット)

EXPSTE:	RTS			; 比較完了、R3は文字列の長さぶんだけ進んでいる
	NOP			; (遅延スロット)

 今見るとソースに統一感がないです。ごめんなさい。 EXPSTR では終了後R3をEXEPに書き戻していません。呼びだし元でやっています。

・QLxOPR グループ

 演算子であるかどうか調べ、結果をTビットに返します。x は演算子のレベルで 1、2、3 のいずれかです。大きいほど優先度が高いです。
 ちなみにCCMBの演算子は以下に挙げる通りです。

レベル1(比較系)

= <> > < >= <=

レベル2(加減算系)

+ - OR XOR

レベル3(乗除算系)

* / AND



 事前のEXEPロードや空白飛ばしは呼びだし元でやっているので L△□、S△□はありません。また、このグループは QEOS と違い、演算子であった場合その長さぶんだけR3を進め、演算子の実行アドレスをR4に返します。
QL1OPR:	CMP/EQ	#H'3D,R0	; '=' なら OPEQ へ
	BT	OPEQ
	CMP/EQ	#H'3C,R0	; '<' なら QL1OP2 へ(まだ確定ではありません)
	BT	QL1OP2
	CMP/EQ	#H'3E,R0	; '>' でなければL1演算子ではない
	BF	QOPN0
	MOV.B	@(1,R3),R0	; '>' ならもう1文字先を見る
	ADD	#1,R3		; R3インクリメント
	CMP/EQ	#H'3D,R0	; '=' 1文字先が '=' なら結局演算子は ">="
	BT	OPGE
	MOV.L	XGT1,R4		; そうでなければ ">" なのでその実行アドレス
	RTS			; R3は1バイト進んでいる
	SETT			; Tビットをセットしてリターン(遅延スロット)

OPGE:	ADD	#1,R3		; 演算子は ">=" なのでさらに1バイト進める
	MOV.L	XGE1,R4		; ">=" の実行アドレス
	RTS			; Tビットをセットしてリターン
	SETT			; (遅延スロット)

QL1OP2:	MOV.B	@(1,R3),R0	; 1文字目が '<' だった、2文字目を見る
	ADD	#1,R3		; R3を1文字進める
	CMP/EQ	#H'3D,R0	; '=' なら結局演算子は "<="
	BT	OPLE
	CMP/EQ	#H'3E,R0	; '>' なら結局演算子は "<>"
	BT	OPNE
	MOV.L	XLT1,R4		; "<" だったのでその実行アドレス
	RTS			; R3は1バイト進んでいる
	SETT			; Tビットをセットしてリターン(遅延スロット)

OPLE:	ADD	#1,R3		; もう1バイト進める
	MOV.L	XLE1,R4		; "<=" の実行ルーチン
	RTS			; Tビットをセットしてリターン
	SETT

OPNE:	ADD	#1,R3		; もう1バイト進める
	MOV.L	XNEQ1,R4
	RTS			; Tビットをセットしてリターン
	SETT

OPEQ:	ADD	#1,R3		; もう1バイト進める
	MOV.L	XEQ1,R4
	RTS			; Tビットをセットしてリターン
	SETT

	ALIGN	4

XGT1:	DC.L	XGT		; 実行アドレス
XGE1:	DC.L	XGE
XLT1:	DC.L	XLT
XLE1:	DC.L	XLE
XNEQ1:	DC.L	XNEQ
XEQ1:	DC.L	XEQ

QOPN2:	ADD	#-1,R3		; 2バイト進めすぎたときここに飛んでくる
QOPN1:	ADD	#-1,R3		; 1バイト進めすぎたときここに飛んでくる
QOPN0:	RTS			; L1演算子ではなかったので
	CLRT			; Tビットをクリヤしてリターン(遅延スロット)

 1文字づつ即値で比較していて、非常に泥くさいコードですが、演算子は1文字か2文字のものが多く種類も少ないので、ループまわして文字列テーブルと比較する、なんてアルゴリズムよりこちらのほうが速そうなのでこうなっています。
 今後もこういう美しくないコードがたびたび出てくると思いますが、皆さんには「こんなダサいコードでも実際に動くインタプリタは作れるんだ。じゃあ僕も、とにかく手を動かしてなんか作ってみよう」という点で励みになってくれれば、と思います。

 レベル2、3の演算子も基本的には同じ構造なのでリストを挙げるにとどめます。
; CHECK LEVEL2 OPERATORS

QL2OPR:	CMP/EQ	#H'2B,R0	; '+' なら OPPLUS へ
	BT	OPPLUS
	CMP/EQ	#H'2D,R0	; '-' なら OPMIN へ
	BT	OPMIN
	CMP/EQ	#H'4F,R0	; 'O' なら "OR" かもしれないので QL2OP2 へ
	BT	QL2OP2
	CMP/EQ	#H'58,R0	; 'X' でなければL2演算子ではない
	BF	QOPN0		; "XOR" か?(以下詳細コメント略)
	MOV.B	@(1,R3),R0
	ADD	#1,R3
	CMP/EQ	#H'4F,R0	; 'O'
	BF	QOPN1
	MOV.B	@(1,R3),R0
	ADD	#1,R3
	CMP/EQ	#H'52,R0	; 'R'
	BF	QOPN2
	ADD	#1,R3
	MOV.L	XXOR1,R4	; 演算子は "XOR"
	RTS
	SETT

QL2OP2:	MOV.B	@(1,R3),R0	; "OR" か?
	ADD	#1,R3
	CMP/EQ	#H'52,R0	; 'R'
	BF	QOPN1
	ADD	#1,R3
	MOV.L	X_OR1,R4	; 演算子は "OR"
	RTS
	SETT

OPPLUS:	ADD	#1,R3
	MOV.L	XPLUS1,R4	; 演算子は "+"
	RTS
	SETT

OPMIN:	ADD	#1,R3
	MOV.L	XMINUS1,R4	; 演算子は "-"
	RTS
	SETT

; CHECK LEVEL3 OPERATORS

QL3OPR:	CMP/EQ	#H'2A,R0	; '*'	; (もう説明しなくても大丈夫でしょう)
	BT	OPMUL
	CMP/EQ	#H'2F,R0	; '/'
	BT	OPIDIV

	CMP/EQ	#H'41,R0	; 'A'
	BF	QOPN0
	MOV.B	@(1,R3),R0
	ADD	#1,R3
	CMP/EQ	#H'4E,R0	; 'N'
	BF	QOPN1
	MOV.B	@(1,R3),R0
	ADD	#1,R3
	CMP/EQ	#H'44,R0	; 'D'
	BF	QOPN2
	ADD	#1,R3
	MOV.L	XAND1,R4
	RTS
	SETT

OPMUL:	ADD	#1,R3
	MOV.L	XMUL1,R4
	RTS
	SETT

OPIDIV:	ADD	#1,R3
	MOV.L	XIDIV1,R4
	RTS
	SETT
OPMUL:	ADD	#1,R3
	MOV.L	XMUL1,R4
	RTS
	SETT

OPIDIV:	ADD	#1,R3
	MOV.L	XIDIV1,R4
	RTS
	SETT

	ALIGN	4

XXOR1:	DC.L	XXOR
X_OR1:	DC.L	X_OR
XPLUS1:	DC.L	XPLUS
XMINUS1:DC.L	XMINUS
XAND1:	DC.L	XAND
XMUL1:	DC.L	XMUL
XIDIV1:	DC.L	XIDIV


・SCAN

 今回のトリは低レベルテキスト解析ルーチンの中では一番複雑な SCAN ルーチンです。といってもループが2重になっているだけで、コードも短く、ある意味 QLxOPR よりソースを追いやすいです。QLxOPR のところで述べた「ループまわして文字列テーブルと比較するアルゴリズム」に該当するのが実はこれです。
 何をするルーチンかというと、R3が指している文字列が "PRINT" や "LIST" のような文・コマンドであるかどうかを調べ、文・コマンドであればその実行アドレスを返します。関数やプリント文キャストの判別にも使われます。

 CMBではさらに内蔵IOアドレス定数の判別にも使われていました。ただしCMBのものは少し構造が違い、ハッシュテーブルを使ってより高速に検索するようになってます。



 このルーチンはR2にネームテーブルを指定して呼びだします。文・コマンドなら文・コマンドのネームテーブル、関数なら関数のネームテーブルを指定します。

 ネームテーブルは実行アドレスと文字列を組にしたエントリがリンクポインタで数珠つなぎにされている、というような構造になっています。SCAN は、この数珠つなぎになったエントリをたどっていってR3が指す文字列と同じ文字列がネームテーブルに存在するかどうか調べるわけです。

 ネームテーブルの1エントリの構造は次のようになっています。

4バイト

リンクポインタ、リンクの終わりなら0

4バイト

実行アドレス

可変長

文字列

1バイト

文字列終わりの '\0'


 これもリンクポインタや実行アドレスはロングワードアクセスされるので先頭は4バイト境界にそろえられます。ちょっとネームテーブルの実例を見てみましょう。いちばん量の少ないPRINT文キャストのネームテーブルです。
	ALIGN	4		; 4バイト境界にそろえる
PL_1:	DC.L	0		; リンクポインタ(リンクの終わりなので0)
	DC.L	XCHR		; 実行アドレス
	DC.B	"CHR("		; 文字列
	DC.B	0		; 文字列の終わりの '\0'

	ALIGN	4		; 4バイト境界にそろえる
PCAST:	DC.L	PL_1		; リンクポインタ、ここからスキャン開始する
	DC.L	UDOT		; 実行アドレス
	DC.B	"USGN("		; 文字列
	DC.B	0		; 文字列の終わりの '\0'

 R2にラベル PCAST の番地を入れて SCAN を呼びだすと、まず "USGN(" という文字列と比較します。一致していたらスキャン成功なので USGN() の実行アドレス UDOT をR2に入れ、Tビットをセットしてリターンします。違っていたらリンクポインタをたどって PL_1 に行きます。文字列 "CHR(" と比較し、一致していれば同様に「成功リターン」します。違っていたらまたリンクポインタをたどるのですが、今度のは値が0で、リンクの終わりです。結局一致するものは無かったのでTビットをクリヤしてリターンします。
 つけ加えると、「成功リターン」の場合はR3をその文字列ぶんだけ進んでいます。Tビットが立たない「失敗リターン」の場合はR3は進みません。

 なぜ PCAST が PL_1 より後ろの方にあるのか? と疑問を持たれた方もいるかと思います。PCAST からスキャンを始めるなら PCAST を先頭に持っていっていちばん最後になる PL_1 を後ろに持っていけばいいじゃないかと。
 確かにそのように書きなおすことも今は可能です。なんでこんな順番になっているかというと、昔作ったMSX用FORTHコンパイラ(BASICよりFORTHの方を先に作りました)の「辞書」の構造を引きずっているからです。FORTHは新しいワードを定義すると辞書が大きな番地の方に伸びていきます。しかも後から定義したワードほど先に検索されます。
 いまだ実現していませんが、私はいつか自己拡張可能なBASICインタプリタを作りたいと思い、ネームテーブルをFORTHの辞書構造に似せたのです。
(組み込み用のH8やSH向けだとRAMとROMとに2つネームテーブルを持つ必要がありますが。新しいキーワードが定義されたらRAMネームテーブルに登録され、SCAN のときはまずRAMネームテーブルを探し、なければROMネームテーブルをさがす、というようにするわけです)



 では SCAN の中身を見ていきます。
SCAN:	MOV	R3,EXEP

 開始部分です。いきなりR3をEXEPに書き戻しています。この SCAN ルーチンは呼びだし側で前もって SKIPSP を実行していることが前提です。スキャン中に文字列の比較に失敗し、次の文字列との比較をはじめる前にR3を元に戻す必要があるため、「暫定値」ともいうべきR3の値をEXEPに書き戻して「確定値」とします。

;(エントリ走査ループ)
SCANLP:	TST	R2,R2		; R2が0ならリンクの終わり
	BT	SCANN		;   結局見つからなかったので「失敗」
	MOV	R2,R1		; R2をR1にコピー
	ADD	#8,R1		; リンクポインタと実行アドレスを
				; 飛ばして文字列の先頭を指す
;(文字列比較ループ)
SCANL2:	MOV.B	@R1+,R0		; ネームテーブル側の文字を読む
	MOV.B	@R3+,R4		; R3が指す文字列の文字を読む
	CMP/EQ	#0,R0		; ネームテーブル側の文字が '\0' なら
	BT	SCANE		;   文字列の最後まで一致したということなので「成功」
	CMP/EQ	R4,R0		; ネームテーブル側とR3側の文字を比較
	BT	SCANL2		; 一致している間は文字列比較ループを繰り返し

	MOV.L	@R2,R2		; 不一致だったのでリンクポインタをたどる
	BRA	SCANLP		; エントリ走査ループへ
	MOV	EXEP,R3		; R3を最初の値に戻す(遅延スロット)

 ループ部分です。エントリ走査ループの先頭に来たとき、R2はネームテーブルの1エントリを指しています(呼びだし元が渡した値、またはループを回ってリンクポインタをたどった結果)。それが0ならそのネームテーブルにはもうエントリがないということなので SCANN へ飛びます。

 0でなければ有効なエントリを指しているので、そのエントリ内の文字列とR3が指す文字列を比較します。R2はリンクポインタを指しているので、文字列はその8バイト先です。R2をR1にコピーして8を足し、以後R3が指す文字とR1が指す文字を1文字づつ比較していきます。

 R1が指す文字が '\0' ならネームテーブル側の文字列を最後まで見たということですから、そこまでの文字はすべて一致したということであり、検索成功ですので SCANE へ飛びます。
 '\0' でなければまだ比較すべき文字列の途中ということですから、その文字とR3側の文字を比較します。同じならその次の文字を比較すべく文字列比較ループの先頭 SCANL2 へ飛びます。

 1文字でも違っていたらそのエントリは明らかに違うので、次のエントリを見に行きます。R2は SCANLP からずっとリンクポインタを指しているので、R2が指しているリンクポインタを読めば次のエントリの先頭番地が得られます(それが0ならもう次のエントリはないわけですが、その判断は SCANLP の先頭で行います)。
 次のエントリの文字列と比較する前に、進んでしまったR3を初期値に戻さなければいけません。最初にEXEPにR3の値を書き戻しているので、それを再度R3にコピーすれば戻ります。それを遅延スロットで行ってエントリ走査ループの先頭 SCANLP に飛びます。

;(成功)
SCANE:	ADD	#-1,R3		; R3は1つ進みすぎているので1つ戻す
	MOV.L	@(4,R2),R2	; 実行アドレスを獲得
	RTS			; リターン
	SETT			; Tビットをセットする(遅延スロット)
;(失敗)
SCANN:	RTS			; リターン
	CLRT			; Tビットをクリヤする(遅延スロット)

 成功した場合は MOV.L @(4,R2),R2 でR2に実行アドレスを取り込みます。R3は最後の MOV.B @R3+,R4 で1バイト進みすぎているので1つもどします。これでR3はちょうどその文字列の長さぶんだけ進んだことになります。さらにTビットをセットしてリターンします。
 失敗した場合はTビットをクリヤしてリターンしています。R3は全く進んでいない状態で戻ります。この場合R2の値は不定です。

 この SCAN ルーチンは、BASICだけではなく他のスクリプト言語や、コマンドインタプリタなどにも応用できます。入力された文字列、あるいはデータストリーム中のキーワードが「コマンド」もしくは「文」であるか調べ、そうであれば実行アドレスを返すという働きをするわけですから、ある意味ではインタプリタの中心であるとも言えます。

 ところで、「SCAN の中で実行アドレスがわかるんなら、その値を返すんじゃなく SCAN の中から呼びだししちゃえばいいんじゃない?」と思った人もいるかもしれません。

 FORTHならそれでも良いのですがBASICだとそうもいかないです。パラメータを一つとる関数を考えましょう。FORTHだと <パラメータ> <関数> みたいに書きますから、関数の実行アドレスがわかった時点ですでにパラメータは確定してスタックに積まれています(コンパイルモードならパラメータを確定するコードがコンパイル済みです)ので、即関数を呼びだすことができます(コンパイルモードなら関数呼びだしコードをコンパイルします)。
 BASICだと <関数>(<パラメータ>) みたいに書きますから、関数の実行アドレスが決まっても、それに渡すべきパラメータの確定はこれからです。パラメータを確定し終わって、右括弧にたどり着いた時点でようやく関数を実行できます。
(ちなみにそのとき、ちゃんと右括弧が存在するか確かめるために EXPRPA グループのルーチンが使われます)

 このへんの事情を見ても、実はBASICよりFORTHの方が自作しやすいことがわかります。でも意外とFORTHって流行らなかったんですよね。不思議です。



 今回はここまでにしようと思います。