英字であるかどうかを調べます。変数名が現れる可能性があるとき、これで英字かどうか調べ、英字なら変数であると判断するのに使います。小文字はどうなるの?という疑問もあるかと思いますが、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' 以上か?(遅延スロット) |
これまでにも何度か出てきたけれど解説していなかったルーチンです。空白以外の文字が現れるまで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 ; (遅延スロット) |
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ビットをセット(遅延スロット) |
右括弧が存在することを期待するルーチンです。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に書き戻す |
ところで、不一致時に飛ぶラベルが JSYNERR2 になっています。CCMBには JSYENRRx というラベルが全部で5個所にあります(CMBはもっと多いです)。これは、BT、BF、BT/S、BF/Sといった条件分岐命令が近くにしか飛べないからです。無条件分岐命令 BRA はもっと遠くに飛べるので、プログラムの各所に BRA SYNERR という命令を散りばめて置き(もちろん必要のないところには置きません)、条件分岐命令は最寄の BRA にジャンプすることでどこからでも SYNERR に飛べます。 |
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に書き戻す(遅延スロット) |
特定の文字列が存在することを期待します。存在しなければ 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 ; (遅延スロット) |
演算子であるかどうか調べ、結果をTビットに返します。x は演算子のレベルで 1、2、3 のいずれかです。大きいほど優先度が高いです。
ちなみにCCMBの演算子は以下に挙げる通りです。
レベル1(比較系) |
= <> > < >= <= |
レベル2(加減算系) |
+ - OR XOR |
レベル3(乗除算系) |
* / AND |
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ビットをクリヤしてリターン(遅延スロット) |
; 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 ルーチンです。といってもループが2重になっているだけで、コードも短く、ある意味 QLxOPR よりソースを追いやすいです。QLxOPR のところで述べた「ループまわして文字列テーブルと比較するアルゴリズム」に該当するのが実はこれです。
何をするルーチンかというと、R3が指している文字列が "PRINT" や "LIST" のような文・コマンドであるかどうかを調べ、文・コマンドであればその実行アドレスを返します。関数やプリント文キャストの判別にも使われます。
CMBではさらに内蔵IOアドレス定数の判別にも使われていました。ただしCMBのものは少し構造が違い、ハッシュテーブルを使ってより高速に検索するようになってます。 |
4バイト |
リンクポインタ、リンクの終わりなら0 |
4バイト |
実行アドレス |
可変長 |
文字列 |
1バイト |
文字列終わりの '\0' |
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' |
なぜ PCAST が PL_1 より後ろの方にあるのか? と疑問を持たれた方もいるかと思います。PCAST からスキャンを始めるなら PCAST を先頭に持っていっていちばん最後になる PL_1 を後ろに持っていけばいいじゃないかと。 |
SCAN: MOV 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を最初の値に戻す(遅延スロット) |
;(成功) SCANE: ADD #-1,R3 ; R3は1つ進みすぎているので1つ戻す MOV.L @(4,R2),R2 ; 実行アドレスを獲得 RTS ; リターン SETT ; Tビットをセットする(遅延スロット) ;(失敗) SCANN: RTS ; リターン CLRT ; Tビットをクリヤする(遅延スロット) |
ところで、「SCAN の中で実行アドレスがわかるんなら、その値を返すんじゃなく SCAN の中から呼びだししちゃえばいいんじゃない?」と思った人もいるかもしれません。 |