ENTPRGを解説する前に・・・

戻る



 さて、BASICプログラムを入力する処理 ENTPRG を解説しようと思うのですが、その前に ENTPRG から呼ばれるサブルーチンを見ていきます。

・LTOUPP

 Line convert TO UPPer の略です。行入力バッファの内容を英大文字に変換します。なぜそうするのかというとプログラムの解釈実行の際「キーワードはかならず大文字になっているもの」としたほうが文字列の比較が速くなるからです。
 クオーテーションの中や注釈は、プログラム実行の際 SCAN や Qxx シリーズのルーチンの比較対象になることはないので変換しません。

 また、このルーチンはプログラム入力の際の行末の空白を取り除く作用もあります。大文字に変換することと、行末の空白を取り除くことは本来全然関係ない処理で、私がごく初期に作ったインタプリタでは別のルーチンになっていました。しかし、どちらもトップレベルから1度づつ呼ばれるだけで、他の処理から呼ばれることはないため、いっしょくたにまとめられました。

LTOUPP:	MOV.L	LINBUF4,R3	; 行入力バッファの先頭番地

 開始部分です。行入力バッファの先頭番地をR3に入れます。

LTOUPL:	MOV.B	@R3,R0		; 1文字読む
	CMP/EQ	#0,R0		; 行末なら
	BT	LTOUE2		;   LTOUE2へとぶ
	CMP/EQ	#H'27,R0	; 注釈なら
	BT	LTOUE1		;   LTOUE1へとぶ
	CMP/EQ	#H'22,R0	; ダブルクオートなら
	BT	LTOUPQ		;   LTOUPQへとぶ
	MOV	#H'61,R13	; 'a'より
	CMP/HS	R13,R0		;   小さければ
	BF	LTOUP1		;     LTOUP1へとぶ
	MOV	#H'7A,R13	; 'z'より
	CMP/HI	R13,R0		;   大きければ
	BT	LTOUP1		;     LTOUP1へとぶ

 ループの先頭です。行入力バッファの内容を1文字づつ読んではそれぞれの処理に分岐します。行末なら行末処理、注釈なら行末までスキップ処理、ダブルクオートならダブルクオート内スキップ処理にとびます。
 英小文字 'a'より小さいか、'z' より大きい場合は大文字変換処理をスキップします。

	ADD	#-H'20,R0	; H'20を引いて大文字にに変換
	MOV.B	R0,@R3		; バッファを書き直す

 英大文字変換処理です。ご存知とは思いますがアスキーコードの英小文字は H'20 を引くと大文字になります。

LTOUP1:	BRA	LTOUPL		; ループ先頭に戻る
	ADD	#1,R3		; R3を1文字進める(遅延スロット)

 ポインタをひとつ進めてループ先頭に戻ります。

; ダブルクオートの場合
LTOUPQ:	MOV.B	@(1,R3),R0	; 次の文字を読む
LTUPQL:	ADD	#1,R3		; R3を1文字進める
	CMP/EQ	#0,R0		; 行末なら
	BT	LTOUE2		;   LTOUE1へとぶ
	CMP/EQ	#H'22,R0	; ダブルクオートなら
	BT	LTOUP1		;   LTOUP1へもどる
	BRA	LTUPQL		; ダブルクオートループに戻る
	MOV.B	@(1,R3),R0	; 次の文字を読む(遅延スロット)

 ダブルクオートの場合は、もう一度ダブルクオートが現れるまで大文字変換をせずポインタを進めます。サーチ中に行末の0が現れた場合は行末処理にとびます。もう一度ダブルクオートを見つけたらループの先頭に戻ります。

; 注釈スキップ
LTOUE1:	MOV.B	@R3,R0		; 1文字読む
	CMP/EQ	#0,R0		; 行末なら
	BT	LTOUE2		;   LTOUE2へとぶ
	BRA	LTOUE1		; 注釈スキップループに戻る
	ADD	#1,R3		; R3を1文字進める(遅延スロット)

 注釈の場合、もうその先、行末まで文などがが現れることはないので行末までポインタを進めます。行末を見つけたら次の行末処理にそのままなだれ込みます。

; 行末の空白トリム
LTOUE2:	ADD	#-1,R3		; 1文字戻す
	MOV.B	@R3,R0		; 1文字読む
	MOV	#H'20,R13	; 空白の文字コード
	EXTU.B	R0,R0		; 読んだ文字の符号無し化
	CMP/HI	R13,R0		; 読んだ文字が空白でなければ
	BT	LTOUPE		;   LTOUPEへとぶ

	MOV	#0,R0		; 空白を行末の0で
	BRA	LTOUE2		;          空白トリムループに戻る
	MOV.B	R0,@R3		;   置き換えて(遅延スロット)

 行末の空白除去処理です。今度は、空白以外の文字が見つかるまでポインタを戻しながら空白を0で置き換えていきます。

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

 リターンします。

・SCNEQL、SCNHSL

 それぞれ、SCaN EQual Line、SCaN High or Same Line の略です。同じ行番号、または等しいか大きい行番号の行を探すルーチンです。 ENTPRG では編集の対象となる行を探したり、入力された行を挿入すべき位置を求めるためにこれらのルーチンを使います。ちなみに、 SCNEQL はGOTO文、GOSUB文で目的の行番号を探すのにも使われます。

SCNEQL:	MOV.L	@(BASTOP-WRKTOP,GBR),R0	; BASICプログラムの開始番地
	MOV	R0,R3			; をR3にロード

SCNELP:	MOV.L	@R3,R1			; 行番号をR1にロード
	MOV.B	@(4,R3),R0		; 行の長さをR0にロード
	TST	R1,R1			; 行番号が0(プログラムの終わり)なら
	BT	SCNEN			;   スキャン失敗
	CMP/EQ	R1,R2			; 目的の行番号と一致したら
	BT	SCNEE			;   スキャン成功
	EXTU.B	R0,R0			; 長さ符号無し化
	ADD	#5,R3			; 行番号と長さをスキップ
	BRA	SCNELP			; ループ先頭に戻る
	ADD	R0,R3			; 次の行の番地を計算(遅延スロット)

; スキャン成功
SCNEE:	RTS				; リターン
	SETT				; Tビットをセット

; スキャン失敗
SCNEN:	RTS				; リターン
	CLRT				; Tビットをクリヤ


SCNHSL:	MOV.L	@(BASTOP-WRKTOP,GBR),R0	; BASICプログラムの開始番地
	MOV	R0,R3			; をR3にロード

SCNHLP:	MOV.L	@R3,R1			; 行番号をR1にロード
	MOV.B	@(4,R3),R0		; 行の長さをR0にロード
	TST	R1,R1			; 行番号が0(プログラムの終わり)なら
	BT	SCNEN			;   スキャン終了
	CMP/HS	R2,R1			; 目的の行番号と一致したら
	BT	SCNEN			;   スキャン終了
	EXTU.B	R0,R1			; 長さ符号無し化
	ADD	#5,R3			; 行番号と長さをスキップ
	BRA	SCNHLP			; ループ先頭に戻る
	ADD	R1,R3			; 次の行の番地を計算(遅延スロット)

 基本的には、プログラムの先頭から順に行を見ていって、条件を満たす行番号を見つかるまで繰り返す、という処理です。これらのルーチンのソースリストを見てもいまいちピンと来ない方は、
行の構造とLISTコマンドの項を復習するとよいでしょう。

・LDIR、LDDR

 名前通り、Z80のブロック転送命令と類似の働きをします。R3に転送元の番地、R2に転送先の番地、R1に転送するバイト数を入れて呼び出します。
LDIR はほぼZ80と同じポストインクリメント動作なのですが、LDDR はプリデクリメント動作になります。結局そのほうが使い勝手がよいからです。

LDIR:	TST	R1,R1		; 転送するバイト数が
	BT	LDIRE		;   0なら即終了

LDIRL:	MOV.B	@R3+,R0		; 1バイト読む
	DT	R1		; 残りバイト数チェック
	MOV.B	R0,@R2		; 1バイト書き込む
	BF/S	LDIRL		; まだ残りがあるならループ
	ADD	#1,R2		; R2インクリメント(遅延スロット)

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

 最初にR1が0かどうか調べ、0なら即リターンします。「R1に0を入れて呼ぶほうが悪いんだからそんなチェックは要らない」という考え方もありますが、やっぱり人間はミスするものですから、うっかりR1が0のまま呼んでしまうようなコードを書いてしまうかもしれません。そういうときにメモリ空間全域を壊してしまうような事態は避けたいです。

 LDDRも転送方向が違うだけで、基本的な構造は同じですが、こちらはポインタをデクリメントしてからデータをアクセスするという、「プリデクリメント動作」になっています。
LDDR:	TST	R1,R1		; 転送するバイト数が
	BT	LDDRE		;   0なら即終了

LDDRL:	ADD	#-1,R3		; R3デクリメント
	MOV.B	@R3,R0		; 1バイト読む
	DT	R1		; 残りバイト数チェック
	BF/S	LDDRL		; まだ残りがあるならループ
	MOV.B	R0,@-R2		; 1バイト書き込む

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


 なぜプリデクリメント動作のほうが使い勝手がよいかですが、それは以下のZ80版CMBの ENTPRG の一部分(行挿入を行う INSLNX )のリストを見てもらえばわかります。LDDRを実行する前にHLとDEをデクリメントしているでしょう。LDDR がプリデクリメント動作ならこの2つのデクリメント命令は要らないわけです。
INSLNX::LD	E,L
	LD	D,H
	ADD	HL,BC		; CALC TARGET ADDR+1
	PUSH	DE
	LD	DE,(ENDFRE)
	AND	A
	SBC	HL,DE
	JP	NC,OUTMEM
	ADD	HL,DE
	POP	DE
	LD	(BASEND),HL
	DEC	HL		; TARGET ADDR ←この DEC 命令2つが
	DEC	DE		; SOURCE ADDR ←なんかもったいない
	EX	DE,HL		; SOURCE -> HL,TARGET -> DE
	LD	BC,(GTLEN)
	LDDR
	JR	REPLNX



今回はここまでとします。