HELP Contents
この章ではPMLアセンブラで利用できるちょっと高度な機能について解説します。ここにある機能は少し大きめのプログラムを組む際にあると便利な機能です。なのでそういう物を作る予定のない方にはすぐには不要なものですが、ざっと眺めておいてもらうと後で役にたつかもしれません。
さて、このたび新装版を出すにあたって、せっかくだからサンプルの追加くらいはしようと思ったわけです。初版時のサンプルは本書のサンプルそのままでいかにも見かけが地味だし、せっかくモニタに色々表示ができるのだから、迷宮をモニタに表示して冒険者がそこから脱出する様がビジュアルに確認できるようにしてやろうとそう考えたわけです。ところが―――
どこの誰がつくったツールじゃ! 使いにくいわっ!
と、いきなりキレました。作ったのは約8年ほど前の自分なんですが―――目的が“偽機械語”でもちゃんと動くんだよ? というデモをするためのものだったので、大きなプログラムを組むことを前提に考えられていません。
そこで今回少し大きなプログラムを組んでも困らないような仕組みを色々導入したわけです。それがこの章で解説するファイルインクルード機能、マクロ機能、命名規約、そして名前空間なのでした。
さて、とりあえず追加サンプルには左手の法則で迷宮から脱出する様をモニタ上に表示することを考えたのですが、それだけだとちょっと寂しいから大きな迷路を作って、スタート位置や向きも指定できるようにして、あと「あてどなき彷徨版」―――補足で言及した「分かれ道に来たらサイコロを振って適当に道を選ぶ」というアルゴリズムについても作ってやろうと思ったわけです。果たして本当に出てこられるのか? 結構手に汗握る見物になるに違いありません。
と、そこで問題になったのが、サブルーチンの共用化でした。
「左手の法則」と「あてどなき彷徨」はキャラの移動アルゴリズムは異なっていますが、迷路の表示やスタート位置設定といった部分は全く同じです。さらに今後、別の脱出アルゴリズムのプログラムを作る際にも利用できそうです。そういった場合どうしたらいいでしょうか?
そんなこと悩む必要なんてないさ! 現在のエディタというのにはコピー&ペーストという機能があるんだから、一つできたらそれをコピーして貼り付けてしまえば一丁上がり……
ということだけはやってはいけません!
なぜかというと、そういうことをするとその瞬間だけは楽でも、後で地獄の苦しみが待っているからです。
例えば脱出のアルゴリズムを変えたプログラムを一杯作って人に見せたとします。その人に壁の色が変だと言われてそれじゃ色を変えてみようかと思ったとします。するとどうなるでしょうか?
そうですね? コピペした部分をみんな一つずつ修正して行かなければなりません。
あなたは文章をコピペした場所をいちいち覚えているでしょうか? 朝食のパンの枚数を覚えていられる人ならばいざ知らず、普通はそんなことあり得ません。すなわちそういった場所を探し出すだけで一苦労になってしまいます。しかもたくさん直しているうちに直し間違えた、なんてことがあるかもしれません。
しかもこれが壁の色程度ならまだいいですが、お金の計算をしているルーチンのバグとかだったらどうでしょう。絶対直さなければならないわけですが―――ところがそういう作り方をしていたら、あなたはその間違いをすべて修正できたと胸をはって保証できるでしょうか? コピペした場所をすべて覚えていなかったかもしれないし、直し損なった場所があるかもしれないのです。
そういうわけでプログラムを作る際には一つの機能は1カ所にまとめるのが大原則です。そもそもサブルーチンという物がそういう目的を満たすために作られたような物でした。
ところが、以前の仕様ではPMLアセンブラは一つのプログラムファイルしか扱えませんでした。その中でならサブルーチンを使って機能の共用化ができましたが、それでは異なったプログラムの間でサブルーチンを共用するにはどうしたらいいでしょうか?
といったことを実現する一番シンプルな方法が、このファイルインクルード機能なのです。
例えば下記のようにProg1.pasmとSub1.pasmを作ります。
<Prog1.pasm>
LD X #15 LD Y #25 INCLUDE "Sub1.pasm" CMP X Y JPT >Z ZF
<Sub1.pasm>
ADD X Y ;XとYを加算 SR X ;Xを2で割る
そしてProg1.pasmを読み込むと、以下のようにINCLUDE疑似命令が書かれた行にSub1.pasmの内容がそのまま、コメントまで含めて挿入されます。
Prog1を読むと
LD X #15 LD Y #25 ▼INCLUDE Sub1.pasm ADD X Y ;XとYを加算 SR X ;Xを2で割る ▲END INCLUDE CMP X Y JPT >Z ZF
こうすることで、色々なプログラムから同じSub1.pasmを参照することができます。そしてSub1.pasmを修正すれば、インクルードしているすべてのプログラムに修正が反映されます。
そしてこうしておけば間違いを修正するような際にも、その機能をプログラムしている場所が1カ所なのだから、そこさえ直っていれば問題はすべて修正されているということが論理的に保証できるのです。
さて、プログラムを作っていると、複数の行から構成された一種の「決まり文句」がよく現れます。例えばサンプルソースにある算術演算サブルーチンの呼び出しでは、以下のようにサブルーチンが呼ばれる前に必ず^X_と^Y_というアドレスに計算したい変数のアドレスをセットしなければなりません。
LD ^X_ @A LD ^Y_ @B CALL >FUNC_
サブルーチンに渡すパラメータをセットしているところなのでこの3行でワンセットになっているのですが、こういうパターンをいちいち書くのは面倒な上にソースコードが長くなって見通しが悪くなってしまいます。
こういう場合にパワーを発揮する機能がマクロ機能です。これは複数行のパターン化された命令を1行で書いてしまえる疑似命令です。例えばかけ算のサブルーチンであれば
&ML_:MACRO ;&ML_という名前のマクロを定義する LD ^X_ @A LD ^Y_ @B CALL >ML_ ENDMACRO
このようにマクロ定義をしておきます。そしてその機能を使いたい場所に例えば以下のようにマクロの名前を書きます。
ADD A #25 &ML_ SUB A #15
そうすると以下のように、名前が書かれた位置にマクロ定義のMACROとENDMACROに挟まれた部分のソースが自動的に挿入されるのです。
ADD A #25 LD ^X_ @A LD ^Y_ @B CALL >ML_ SUB A #15
言いかえると、マクロ機能を使うことで、3行に渡って書かなければならなかった命令群が、あたかも一つの命令であるかのように書けるようになるわけです。
このあたりはインクルードと考え方がちょっと似ています。しかし大きな違いは、インクルードはファイルを一つ丸ごと挿入するのに対して、マクロはマクロ定義した部分だけが挿入されます。またインクルードの際には同じファイルを複数回インクルードすることはできません。
なので小さな決まり文句のようなパターンにはマクロ、サブルーチンやラベルの定義のようなものはインクルードというように使い分けることになります。
このようにマクロは便利な機能なのですが、でもこれだけではまだあまり使えません。
というのはプログラムには似たような処理は多いけれど全く同じ処理というのは意外に少ないからです。
前の例を見てもらうとA,Bというラベルが固定して使われています。しかしかけ算というからには任意のパラメータ同士で使えてくれないと嬉しくありません。ラベルが変わるごとにマクロを作っているのでは余計面倒です。
そこでマクロにはマクロパラメータもしくは仮パラメータというのがつけられます。例えば以下のようにマクロを定義しておきます。
&ML_:MACRO %1, %2 LD ^X_ %1 LD ^Y_ %2 CALL >ML_ ENDMACRO
て、マクロを使いたいところで以下のように書きます。
&ML_ @A @B
するとアセンブラがこの行を
LD ^X_ @A LD ^Y_ @B CALL >ML_
%1、%2と書かれているところを@A、@Bに置き換えて展開してくれるのです。
ところでこれを見て高級言語の関数のことを思いだした人がいるかもしれません。実際見かけは本書の高級言語のところで説明した関数に似ています。
高級言語だと例えば以下のような関数定義と呼び出しができます。
function ML(A, B:integer):integer; begin Result := A * B; end X := ML(A, B);
これを見ると処理内容をあらかじめ定義しておいて、それを名前一発で呼び出せるあたりはよく似ています。
しかし似ているのはそこまでで、マクロ命令は高級言語の関数とは本質的に異なっています。
高級言語の関数は機械語のサブルーチン、すなわちCALLとRETの命令の組み合わせをこのように表現できるようにしたものです。しかしアセンブラのマクロとは実は裏でこっそりアセンブラがマクロ定義されたテキストの行をそこにコピペして、変数に相当するところを検索して置換したソースを作っているのです。
そのため機械語になったときにはもうマクロがあったかどうかなんて分かりません。サブルーチンは何度呼び出してもソースサイズは増えませんが、マクロはその度にコピペされるのでプログラムがどんどん大きくなっていきます。
そういう特徴があるので、マクロは比較的小さな決まり文句のような処理に適しています。大きな処理をしたいときにはサブルーチンを使った方がよいでしょう。
マクロが便利なのは手間を省くことだけではありません。
もう一つ例を挙げます。アセンブリ言語で条件分岐をするのは簡単なようで結構頭がこんがらがります。例えばPMLではX<Yの場合>Lにジャンプしたいという場合は下記のように書きますが……
CMP X Y JPT >L SF
多分すぐJPTだったかJPFだったか混乱してくることでしょう。そこで
&IFLT_:MACRO %X,%Y,%L CMP %X %Y ; IF %X Less Than %Y goto %L JPT %L SF ENDMACRO
といった調子で覚えやすい名前で定義しておけば
&IFLT_ X, Y, >L ; IF X Less Than Y goto >L
というようにもうどのジャンプ命令を使えばいいか悩む必要はなくなります。
MACRO疑似命令のあとに続くマクロパラメータは必ず%もしくは?で始めます。
%で始めたパラメータは前節で解説したように、マクロが呼び出されたときに実パラメータで置き換えられます。
?で始めたパラメータはオートラベルといってアセンブラが適当なラベルを生成します。
適当なラベル? と思った方がいるかもしれません。でもこれが結構便利なんです。
マクロの中にジャンプ命令とその飛び先までがあるなんてことはすごくありがちです。
例えば「直前の計算結果が0より大きければ>Lにジャンプする」という機能をもったマクロを作りたいとします。すると(実はこれが結構面倒だったりするんですが)以下のような命令のセットになります。
SUB X Y JPT >L1 ZF JPT >L1 SF JP >L L1: ;結果が0以下だった場合の処理 L: ;ここから結果が0より大きかった場合の処理
見てもらうと分かりますが、この条件分岐をするためには">L1"という新しいラベルが必要です。ところがマクロの場合マクロソースがコピペされるわけですから、普通に書いたらこのラベルまでコピペされて、ラベルの二重化ですといったエラーが発生してしまいます。
そこで―――
&IFP_: MACRO %L0, %L1 JPT %L1 ZF ;結果 > 0なら%Lへ JPT %L1 SF JP %L0 %L1: ENDMACRO
このように%L1というパラメータとして与えてやれば二重化は回避できますが、この程度の条件分岐は色々な場所で発生し得ます。そのたびにいちいち新しいラベル名を考えるというのは不毛です。
そこで以下のようにマクロ定義します。
&IFP_: MACRO %L, ?L0 JPT ?L0 ZF ;結果 > 0なら%Lへ JPT ?L0 SF JP %L ?L0: ENDMACRO
そしてこれを次のように呼び出します。
SUB X Y &IFP_ >L ;結果が0以下だった場合の処理 L: ;ここから結果が0より大きかった場合の処理
こうすると
SUB X Y JPT *0000 ZF JPT *0000 SF JP >L *0000: ;結果が0以下だった場合の処理 L: ;ここから結果が0より大きかった場合の処理
というように?Lのあったところには*0000というラベルが埋め込まれるのです。
これをオートラベルといって、この0000のところは16進数の数字になっていて、呼ばれるたびにアセンブラは違った値を設定します。すなわちこのようなどうでもいいラベルにをアセンブラに自動的に作らせておくことができるのです。
アセンブリ言語でちょっと大きなプログラムを組んでいると、すぐにラベルの数が膨大に増えて何が何やら分からなくなってきます。まあ、そこでラベルビューに選択されたラベル以外非表示というモードもつけたわけなのですが、でも膨大なラベルの中から目的のラベルを探し出すだけでも大変です。これはどうにかならないでしょうか?
これは部屋が散らかってしまって始末に困っているのと同じです。そういう場合どうすればいいかというと、まずは分類整理して不要な物を片付けるというのが正解です。
実際よく見てみるとラベルは使い方によって種類があることがわかります。
もともとラベルとは数値に紐付けされた文字列以上でも以下でもないのですが、使っているうちに運用上の違いというのがいろいろ出てくるわけです。
まず第1に、値をそのまま定義しているものがあります。例えば迷宮脱出では東西南北の方向を0, 90, 180, 270 などと定義しました。それに分かりやすくDIR_N,DIR_Eなどといった名前をつけたものがあります。このタイプを即値型ラベルと呼ぶことにします。
第2にいろんな値の入ったメモリメモリのアドレスを定義しているものがあります。迷宮脱出ではキャラの向きを入れていたり、計算のために値を保持しておくメモリなどがありましたが、こういうタイプも様々な局面でよく使われます。これを変数型ラベルと呼ぶことにします。
続いて、ジャンプ命令やCALL命令の飛び先を示すために行につけられるラベルがありますが、これは行ラベルと呼んでいました。
そしてもう一つ最後に、これは変数型ラベルの一種なのですが、メモリアドレスが入っているメモリを示すラベルがあります。迷宮脱出ではキャラの位置などがこれに相当しますが、これをポインタ型ラベルと呼ぶことにしましょう。
この違いは実は結構重要です。というのは、この型によって機械語の命令としては問題なくとも運用上は無意味な場合が起こり得るからです。
例えば"N"というのが北の方向(すなわち0)を定義した即値型ラベルだとすると―――
LD X N ;× LD X #N ;○ LD X [N] ;×
上記の3つの使い方すべて機械語的には問題ありません。でも1番目と3番目の命令にはどんな意味があるでしょうか?
よく見れば―――全く意味がありませんね? Nの値は0ですが、1番目の命令とは要するに0番地に書かれている値をXに読み込めという命令です。3番目に至っては、0番地に入っている値をアドレスと見て―――という間接参照をしています。もはや何がしたいのかさっぱり分かりません。
すなわち即値型ラベルとはその目的を考えれば、即値として参照するときにのみ意味を持ち、それをアドレスとみて参照するような行為には全く意味がありません。
では変数型ラベルの場合はどうでしょう? "M"というのが、例えば割り算の余りを保管しておくメモリアドレスを指すラベルだったとします。すると―――
LD X M ;○ LD X #M ;○ LD X [M] ;×
最初の命令はMの指すメモリからXの指すメモリに値を読み込んでいます。これはごく普通の行為です。2番目はMというラベルの値そのもの、すなわちMのアドレスをXに読み込んでいます。マルチワード数などだとメモリアドレスを色々処理したいことも起こり得ますから、これも許容範囲です。
でも3番目は一体なんでしょう? Mというのは割り算の余りだというのに、その値をアドレスと見て参照することにはどんな意味があるでしょうか? そうです。Mというのが変数型である以上は、意味がありません。
同じく今度は"L"という行ラベルがあったとします―――
LD X L ;× LD X #L ;× LD X [L] ;×
上記の例ですが―――行ラベルというのはそもそもジャンプ命令やCALL命令以外のところで使う物ではありません。すなわちすべて無意味です。
最後にポインタ型ですが、"P"というのが例えば迷路の冒険者のアドレスを保管しておく変数だとすると―――
LD X P ;○ LD X #P ;○ LD X [P] ;○
最初の例は冒険者の今いるアドレスを取得していますが、もちろんこれは正当な利用です。2番目の例は変数型のとき同様にPのアドレスを参照しているわけで、これも許容範囲です。そして3番目の命令には、その場所が道か壁かを取得するというとても重要な意味があります!
ポインタ型とはアドレスの値を持つ変数型であるがゆえに、このポインタ型ラベルだけが、即値、アドレス参照、間接参照すべてに対して意味を持ち得るわけです。
ここまで書けばもう、上記の4種類のラベルをごっちゃにしたら色々まずそうだということがお分かりではないでしょうか。
そうなのです。実際とってもまずいのです。話は最初ごちゃごちゃしているから分類してみようというところから始まったのですが、何だかそれどころではなくなってきました。
実際即値型ラベルをアドレスとして参照してみたり、変数型ラベルを間接参照してしまったら意味がないどころか、極めて分かりにくいバグの原因になってしまうのです。
というのは、メモリの中には様々な値が入っています。そのため即値ラベルをアドレスとして参照してしまったのだけど、たまたま妥当な値が入っていたため一見正常に動いてしまったりすることがあるのです。
正直エラーで止まってくれた方がまだマシです。なぜなら止まってしまえばそこに何か問題があることが分かるからです。でもこのように一見正常なふりをして動かれてしまうのが一番始末に困るのです。なぜならたまたま妥当な値が入っていたというのであれば、何らかのきっかけでダメな値に変わることだってあるかもしれません。そうするとどういう現象になるかというと―――普段はうまく動いているがたまに変な動作をしたり、テストするたびに現象が異ったりという原因を非常に特定しづらいバグの原因になってしまうのです。
そういうことになってしまったのも、うっかり意味のない参照をしてしまったせいでした。そこでこういう問題を未然に防ぐにはどうすればいいかというと―――そうです。見ただけでラベルのタイプが分かるような名前をつけておけばいいわけです。
そこでこのPMLアセンブラでは以下のようなルールでラベル名をつけることにします
こうしておくとまず当初のラベルが多すぎてごちゃごちゃする、という問題がかなり軽減されます。というのはアセンブリ言語でプログラムを組んでいくと、増えるラベルはほとんど行ラベルです。でも行ラベルというのはいちいちその値がどうかなんて気にする必要のないラベルです。そこでラベルビューに「>から始まるラベルは表示しない」という機能をつけるだけで、一気に表示されるラベルが減って見やすくなります。
さらに即値型に関してもこれはいちいち数値を気にしたくないからラベル化したわけで、普段ずっと表示しておく必要はありません。
ラベルを分類して名前の付け方を決めただけで―――これを命名規約といいますが―――これだけすっきりしたわけです。
そしてそれ以上に重要なポイントとして、このように命名規約を導入することで、プログラムのうっかりミスを劇的に減らすことができるのです。
このように名前をつければ、ほぼ必然的に以下のようなルールが発生します。
どれもよく見れば自明なことです。第1パラメータは計算結果を保管したりするためにも使われるので、普通はメモリアドレスでなければなりません(OUT命令に限って、ポート番号を直接指定するために即値が使われることがありますが)
また行ラベルに関しても同様です。ジャンプ命令やCALL命令以外のところに>付きのラベルが現れた瞬間に、何か絶対変だということが分かります。
そして3番目についてはとくに間違えやすいところですが、これで少なくとも変数型のラベルをうっかり間接参照してしまう間違いは自動的になくなります。
こんな風に見ただけで間違いかどうか分かるようになる、というのは命名規約を導入しなければあり得ない話でした。
ちなみに、#という記号はは前にも書きましたが、パラメータが即値かどうかを表す記号であって、ラベルの一部ではありません。従ってラベル定義する際には#を含めることはできないというのが字義通りの仕様です。しかしver1.10で、EQUでラベル定義する際にのみ#をラベル名にをつけられるように修正しました。
まあ、#なしで定義して、参照する場合には間違いなく#をつけるようにしろ! ということも可能だったんですが―――もしEQU定義が必ず即値で使う値を定義するというのであればそうしたんですが、でも自分でサンプルプログラムにEQUでアドレスを定義したりしているし……(笑)
そこでEQUでラベル定義するときにのみ定義時に#をつけることを可能にして、そうやって定義されたラベルには即値であるという気持ちがこもっていると判断して、即値以外の扱いをしたらエラーが出るようにしています。
命名規約の導入で発生するあり得ない組み合わせというのはまだあります。
ここにX,#Y,^Zという3種類のラベルが定義されているとします。もちろんXは変数型、#Yは即値型、^Zはポインタ型ですが、それでは下のような書き方に意味があるでしょうか?
LD X #Y ;@ LD X ^Z ;A LD ^Z X ;B LD ^Z #X ;C LD ^Z #Y ;D
問題集でもクイズの本でもないのですすぐ答えを書きますが―――
LD X #Y ;@ → ○ LD X ^Z ;A → × LD ^Z X ;B → × LD ^Z #X ;C → ○ LD ^Z #Y ;D → ×
@はXという変数型に即値を代入しているので問題ありません。
AはXという変数型にアドレス値を代入していますが、変数型という以上、その中にはアドレスでない何かの値が入っているはずです。したがって目的に合わないためNGです。
Bも同様に^Zというポインタ型、すなわちアドレスを入れるのが目的のメモリに、アドレスでないはずの値を入れているのでNGです。
ここまでは問題ないと思いますが―――
Cは^Zというポインタ型にXという変数型ラベルの即値、すなわちXのアドレスを入れているので意味があります。
Dは^Zというポインタ型に即値型ラベルの値を入れているが、これはもちろん意味がありません。
@〜Bはまあ納得のいく話だと思いますが、CとDがすごく紛らわしいですね。
命名規約を導入して便利だった点が何かというと、その命令に意味があるかないかが一目見るだけで分かってしまうことにありました。でもCとDはラベルを定義しているところまで見に行かないと正しいかどうか分かりません。
これに関しては今までの仕様ではどうしようもありません。がんばって間違えないように注意して下さいということなのですが―――自分で間違えまくってキレたので、下記のような新しい記法を導入することにしました。
今までパラメータが即値であることを示すために"#"という記号を使いましたが、これと全く同じ機能を持ったもう一つの記号"@"を導入することにします。すなわち、
LD ^Z #X LD ^Z @X
上記の命令は全く同じ機能を持っていて、共にXのアドレスを^Zに代入します。そして、この@というのは変数型ラベルのアドレスをポインタ型に代入するときのみ使うという自分ルールにしておくのです。そうするとどうなるかというと
LD ^Z @X ;(4) ○ LD ^Z #Y ;(5) ×
このように一目見るなり区別がつくようになりました。
前節で@という記号は変数型ラベルのアドレスをポインタ型に代入するときのみ使う、と説明しましたが、ポインタ型ラベルというのは変数型の一種と捉えることもできるので、ポインタ型ラベルに対して使用することももちろんできます。すなわち以下のような命令もOKのはずです。
LD ^Q @^P
でもちょっと考えてみて下さい。この^Qってなんでしょうか?
ポインタ型ラベルというのは、メモリアドレスを保持しているメモリのアドレスでした。すなわち保持しているアドレスは変数型アドレスで、その中には色々な計算結果などの値が入っています。上の例で^Pというのがそういうものだとしたら、^Qというのは^Pのアドレスを保持するために使いたいメモリ、すなわちポインタのポインタということになるわけです。
状況によってはそういう物を使う必要が出てくることもあります。でもこれは明らかに普通のポインタ型ではありません。なぜなら普通のポインタ型ならば間接参照したら実データが出てきますが、このポインタのポインタ型は間接参照しても出てくるのはアドレスです。そのアドレスを参照して初めて実際の値が出てきます。
なのでポインタのポインタ型というのを使いたければ^^Xというように^を二つつけておくことにしましょう。こうしておくと
LD ^^X @^Y
はOKですが
LD ^^X @Y
は何かがおかしいということが分かります。ポインタのポインタに変数のアドレスを入れているわけですが、ポインタのポインタということは間接参照してもまだアドレスなのに、これだと間接参照すると値が出てきてしまうからです。
と、この節をこのあたりまで読んだところで―――
わけわからん!
と、うなっている方も多いでしょう。そもそもポインタのポインタってどういうときに使うんでしょうか?
一つ例をあげれば、例えばメモリ中に人の名前がたくさん並んでいて、それを並べ替えたいような場合があります。そういう場合よく名前そのものを並べ替えるのではなく名前への参照テーブルという表を作って、その表を並べ替えるようなことを行います。その表というのは要するにポインタ型のデータがずらっと並んだ表になります。そしてその表に対して様々な処理をしようとすると―――ポインタのポインタを処理する羽目になるのです。
正直ポインタというのは初心者でなくとも混乱しやすい要素です。ここまでの議論をすらっと呑み込めた人はそうはいないんじゃないでしょうか? もちろんそれが正常です。普通の人は混乱して当たり前です。そこで高級言語と呼ばれるものではこのポインタというものが直接には現れてこないように色々工夫されているんですが、でも機械語を扱うのではしかたありません。うまく名前をつけてやることでなるべく混乱しないように努力するしかありません。
さて、前節では命名規約というものを導入してこれがなかなか便利な物だということを説明しましたが、この話の当初の目的である「ラベルの数が多すぎて困る」という件に関しては依然問題が残ったままです。確かにラベルビューで不要なラベルを非表示にすることですこし見かけがすっきりしましたが、でもまだまだラベルが多いことには違いありません。
特に問題なのが行ラベルです。というのは、機械語の場合ちょっとした条件判断をするにもラベルが必要になりますが、当然ながら行ラベルとはジャンプなどの飛び先を指示する物ですから、命令ごとに違っていないと意味がありません。それで結局>L00, >L01, L201, ... などといった名前のラベルが大量生産されることになります(まあそれでもアドレスを直接書くよりはマシですが……)
しかもそういったラベルが何を示しているかというと、ほとんどが条件分岐か繰り返し処理のときのラベルです。そういうかなりどうでもいいようなことのために、人間様が間違えないようにダブらないように名前を考えてやる、というのは少々本末転倒です。名付け親になるというのはやはり大切な相手だけにするのが普通です。これがマクロの中だとオートラベルが使える場合もありますが、これも使える局面は限られています。
そこで同じラベル名をあちらこちらで使い回しても問題が起こらないようにする仕組みがあると便利です。そのために導入した仕組みがPROC疑似命令とUNIT疑似命令です。
この機能をひとことで言えばこの疑似命令で囲まれている範囲内で定義されたラベルは原則としてその範囲内でのみ有効になるということです。
以下はリファレンスでも出した例ですが―――
#X EQU #10 ;(1) FN: PROC #X EQU #20 ;(2) LD P #X ;(3) LD Q #X ;(4) P: DW 0 ;(5) ENDPROC LD P #X ;(6) P: DW 0 ;(7) Q: DW 0
この例ではXおよびPというラベルが2カ所で定義されています。
このように一定の範囲内だけで有効なラベルであるということで、PROCとENDPROCに挟まれたところに定義されているXとPをローカルラベルと呼ぶことにしましょう。
例えばマルチワードの足し算のサブルーチンがあったとします。するとそれは大雑把には以下のように、ワード数のカウンタを持って各ワードの数だけの足し算の処理を繰り返す、という構造を持っています。
>ADD: LD C WC ;ワード数カウンタを設定 >LA: 各ワードの足し算の処理 SUB C #1 ;カウンタを1減らす JPF >LA C ;カウンタが0でなければループ RET
この構造は引き算になっても同様です。それなのにローカルラベルの仕組みがなければ中で使っているラベルは別な名前をつけなければなりません。
>SUB: LD C WC ;ワード数カウンタを設定 >LS: 各ワードの引き算の処理 SUB C #1 ;カウンタを1減らす JPF >LS C ;カウンタが0でなければループ RET
しかしたかがループのラベルにいちいち特殊な名前を考えるのは面倒です。そこで今回導入したPROC疑似命令を使えば以下のように同じラベルを使えます。
>ADD: PROC LD C WC ;ワード数カウンタを設定 >LOOP: 各ワードの足し算の処理 SUB C #1 ;カウンタを1減らす JPF >LOOP C ;カウンタが0でなければループ RET ENDPROC >SUB: PROC LD C WC ;ワード数カウンタを設定 >LOOP: 各ワードの引き算の処理 SUB C #1 ;カウンタを1減らす JPF >LOOP C ;カウンタが0でなければループ RET ENDPROC
PROC疑似命令のラベルは通常の行ラベルと同様に機能します。従ってこのように書いておけば
CALL >ADD CALL >SUB
と呼びだすことでこのサブルーチンが呼び出せます。
しかも、同じ名前であるということはそこにこもっている気持ちも同じであることを意味します。上の例ならこのラベルはループをするためのラベルだということが一目瞭然です。でもその前の例にあった>LAとか>LSでは、それがループ先を示しているのか、それとももっと別のところに飛んでいこうとしているのか、一瞬では分からない場合もあるでしょう。
ラベルの数を減らすということは、その一つ一つに込められた意味を大きくして、分かりやすくするという役割もあるのです。
さて、ここでこのローカルラベルというのをどう実現しているかの説明をしておきます。
PROCを使えば同じ名前のラベルが複数定義できるわけですが、それが本当に同じ名前だったとしたらコンピューターだって区別はつきません。それでどうして区別ができるかというとそれは見えている部分がすべてではないからです。
実は内部処理的にはすべてのラベルにはさらにネームパスという文字列が自動的に付け加わっているのです。それがどんな文字列になるかというと以下の例のようになります。
#X EQU #10 ; X.* FN1: PROC ; FN1.* #X EQU #20 ; X.FN1.* FN2: PROC ; FN2.FN1.* LD P #X ; ☆1 LD Q #X ; ☆2 P: DW 0 ; P.FN2.FN1.* @ ENDPROC ENDPROC P: DW 0 ; P.* A Q: DW 0 ; Q.* B
まずPROCの外では、ラベルに".*"がくっついています。PROCの中では".FN1.*"というようにそのPROC名がつけ加わっています。さらにPROCが二重の入れ子になっている場合は、".FN2.FN1.*"のようになっています。
このつけ加わった文字列がネームパスで、ラベルの真の名前は右に記したように、ラベル名+ネームパスになっています。
さてここで☆1のところのLD命令ですがこれはPに値を入れています。そのときその命令はFN2というPROC内にあり、FN2はFN1というPROC内にいますから、Pというラベルが出てきた場合にはまず"P.FN2.FN1.*"というラベルを探します。
この場合はそのラベルがあるので@の方のPが参照されます。
☆2のLD命令はQに同じく値を入れていますが、この場合もまず"Q.FN2.FN1.*"というのを探します。でもそんなラベルはありません。そこで今度は"Q.FN1.*"というのを探しますがこれもまたありません。続いて"Q.*"というのを探すとこれはありました―――という調子でBのQに正しく代入ができるのです。
ラベルビューの一番右のカラム「パス」というのはそのラベルのネームパスが書かれていて、ラベル.パスという文字列を作れば、そのラベルの真の名前になります。
このようにローカルラベルが使えれば他のところでどういう名前を使っているかを気にせずにラベルに名前がつけられます。しかし状況によっては、PROCの中からPROCの外側の同名のラベルを参照したかったということもあり得ます。
そういった場合には下記の例のようにネームパスまでつけた真の名前を直接指定すればそのラベルを参照することができます。
FN1: PROC FN2: PROC LD P #X ; PROC内のPを参照 LD P.* #X ; 外側のPを参照 P: DW 0 ENDPROC ENDPROC LD P #X ; 外側のPを参照 LD P.FN2.FN1.* #X ; PROC内のPを参照 P: DW 0
これは逆もまた真なりです。すなわちPROC外からPROC内のPを参照したければ、"P.FN2.FN1.*" と指定することでPROC内で定義されているPをアクセスすることが可能です。
PROCを定義する際にはラベルは必ずしも必要ありません。しかしネームパスの理屈からいったら名前がないと困ります。そこで名前なしのPROCがあった場合は下記のようにアセンブラがそこにオートラベルを生成します。すなわち―――
PROC P: DW 0 ENDPROC
というのがあればマクロでも出てきたように*16進数のオートラベルが生成されます。
*0000:PROC P: DW 0 ; P.*0000.* ENDPROC
さてPROCの他にローカルラベルを生成するUNIT疑似命令というものがあります。
これもPROC同様に、UNITとENDUNITとで囲まれた範囲で定義されたラベルはローカルラベルになります。UNIT内にあるラベルにはPROC同様にネームパスがつきます。またUNIT内にあるPROCはUNIT名のついたネームパスがつきます。
UNIT1: UNIT ;UNIT1.* FN1: PROC ;FN1.UNIT1.* P: DW 0 ;P.FN1.UNIT1.* ENDPROC P: DW 0 ;P.UNIT1.* ENDUNIT
UNIT内のラベルを外部から参照する場合、PROCで説明したように P.UNIT1.* と書けば参照することができます。しかし、以下のようにUSES疑似命令をあらかじめ書いておくと、いちいちネームパスをつけずにただPと書くだけでユニット内のラベルPを参照できるようになります。
USES UNIT1
UNIT疑似命令は例えばたくさんのサブルーチンをひとまとめに大きく管理したいような場合に使うと便利です。
さてこのようにあちこちに同じ名前が定義できるわけですが、同じ名前が何カ所にもある場合、どういった優先順位で参照されるかが問題になる場合があります? PMLアセンブラのPROC、UNITのシステムでは下記のような優先順位になります。