AARASS-REGIマクロ表現仕様

本記事はAARASS-REGIのマクロ表現の仕様について述べる。

1章. 概要

1.1 概要

AARASS-REGIマクロ表現は、AARASS-REGIコアマシンで挙動するある程度実効的なプログラムを書くための助けとするための拡張表現である。

1.2 非単純拡大性

マクロ表現と拡張命令表現の関係は、拡張命令表現とコア命令表現の関係とはきわめて異なっている。

ある拡張命令表現、それどころかあるコア命令表現はマクロ表現として非妥当である。

そのためマクロ表現は拡張命令表現の単純な拡大ではない

1.3 マクロ表現の仕様策定方針

マクロ表現は各AARASS-REGI実装が自由に拡張してかまわない。ここで要求する機能は最低限の要求としての意味しかない。

また要求する機能の効果もごく大まかに指定するにとどめる。実際の効果は実装に任せる。

2章. 追加・変更される概念

2.1 行

マクロ表現では行の概念が拡張される。BNFで書けば以下の通りである。

<ラベル無し行>:=<命令>|<宣言文>|<ブロックジェネレータ>

<行>:=<ラベル無し行>|<ラベル形式><ラベル無し行>

拡張命令表現の時と同様、実装は複数のラベルがある行を許すよう拡張してもよい。

2.2 シンボル

シンボルは直感的に言えば文字列の事である。

マクロ表現ではユーザーが自分の理解の助けとなる名称をレジスタやブロックにつけることが可能になるため、それらの名前として用いられる。

2.3 宣言文

宣言文は静的に解析され、それ以降のプログラムで有効になるある種のマクロを定義するものである。

宣言文は命令ではなく、静的に解決される。

ブロックジェネレータが作るブロックを超えて宣言文の効果が伝播することはない。

2.4 ブロックジェネレータ、ブロック

ブロックジェネレータにはさらに「オープンブロックジェネレータ」と「クローズブロックジェネレータ」が存在する。

対応する「オープンブロックジェネレータ」と「クローズブロックジェネレータ」に囲まれたプログラムの領域を「ブロック」と呼び、ブロックを超えて宣言文の効果は伝播しないという性質を持つ。

直感的にはブロックとは「ひとまとまりの手続き」の事である。

ブロックには「手続き名」が与えられる。

ブロックの中でさらにブロックを作ろうとした場合の効果は未規定である。実装はこれを高級言語におけるローカル関数のように解釈することにしてもよいが、それを要求はしない。

3章. マクロ表現の仕様

3.1 コア命令表現及び拡張命令表現では要求されたがマクロ表現ではサポートを免除される仕様、およびプログラマへの注意事項

以下のような点でマクロ表現の仕様はコア命令表現や拡張命令表現のそれと異なる。

1.継続の表現としての即値の不許可

マクロ表現は「継続の表現」に即値を使うことを許可しなくてよい。

これはマクロ表現を拡張命令表現にコンパイルする際には行数がずれる為である。

また同じ理由で、プログラマは継続をレジスタに積む場合などでも、マクロ表現の行数を数えて行うのはバグの可能性があることを知るべきである。特殊な場合を除き継続を表す数が欲しい場合は、いつでも必ずラベルを用いて行うことが求められる。

2.レジスタおよび継続の0以外への初期化の許容

マクロ命令表現のコンパイラーはプログラマーにとって不可視な方法でレジスタや継続の「見た目上の初期値を0以外」にするような形式にコードをコンパイルすることが許されている。

プログラマーは必要に応じて明示的に初期化をする必要があるかもしれない。

3.内部処理用のレジスタ・ラベルの存在

マクロ表現をコンパイルして得られる表現はプログラマが明示したレジスタの挙動だけでなく、マクロ表現に要求される挙動を達成するための内部処理にもレジスタを用い、その値を変更するであろう。ラベルも同様である。

実装は内部処理に用いるレジスタプログラマが安全に用いることができるレジスタを区別できる方法を提供するべきである(例えばプログラマに奇数の番地のレジスタを使うことを要求するなど)。しかしこのような工夫のいずれかを仕様として要求するわけではない。実装は、コンパイラーが作ったのでない命令が内部処理に用いるレジスタを破壊することをエラーとしてもよい。が、これも要求はしない。

同様にラベルについてもプログラマーが安全に使えるラベル表現を指示するべきである。

逆にプログラマは内部処理に用いるレジスタを破壊した場合、あるいはそのようなラベル表現と同一のラベル表現を用いた場合、この記事の、あるいは実装の保証する仕様通りの挙動が得られるとは限らない。これを避けるのはプログラマの責任である。

3.2 hold宣言

hold <シンボル>

という形式は宣言文でありhold宣言と呼ばれる。

hold宣言はプログラムまたはブロックの先頭、あるいはほかのhold宣言の直後でのみ妥当である。

hold宣言は受け取ったシンボルに未使用のレジスタを対応させ(これをレジスタをシンボルにholdするという)、それ以降のプログラムでレジスタ番地として整数を指定する代わりにそのシンボルを使うことを許可するマクロを生成する。

同じブロック内で、もしくはプログラム先頭で複数回同じシンボルにレジスタがholdされることの効果は未規定である。実装はエラーを通知しても、単に無視してもよいし、そのたびに新たにレジスタを割り付けてもよい。

実装はシンボルを使ったレジスタ、値、継続の表現の為に番地を直接使ったものとは異なった文法を用意してもよい。

実装は初期状態ですでにレジスタをholdしているシンボルを用意してもよい。またそのような形でholdされているレジスタを0以外に初期化することにしてもよい(逆にシンボルにholdされていないレジスタを0以外に初期化することは避けるべきである。が、厳密に禁止はしない。)。

例としてコア命令表現の仕様記事の冒頭に貼ったRepl.itで読める僕の実装は初期状態で0番地をreturn(これを関数の返り値を渡すのに使っている)、4番地をstackpointer(内部で関数スタックを組むために使っている)、8番地をtemp1、12番地をtemp2というシンボルに束縛しており、stackpointerは0でなく2に初期化される。

また実装はプログラマがholdされているレジスタを気づかずに使うことを防ぐため、holdされる可能性があるレジスタとないレジスタを区別できる工夫をするべきである(例えばholdは4の倍数番地のレジスタしか確保しないとするなど)。しかし、そのような工夫のいずれかを仕様として要求するわけではない。

またhold宣言が確保したレジスタが0に初期化されることは要求されない(がそういう実装があっても良い)。そのレジスタはもし以前に他の処理に使われていたならばその時の値がクリアされずに残っているかも知れない。

3.3 begin_defun,end_defun

begin_defun <シンボル(関数名)> <シンボル(引数名)>

<行>*

end_defun <シンボル(関数名)>

という形式でブロックを生成できる。このブロックにはbegin_defunの後ろに続くシンボル(関数名)が「手続き名」として割り当てられる。

このとき

begin_defun <シンボル(関数名)> <シンボル(引数名)>

はオープンブロックジェネレータ、

end_defun <シンボル(関数名)>

はクローズブロックジェネレータである。

「引数名」にあたるシンボルは後述するcall命令で値を書き込まれることを除いては、begin_defunの直後にレジスタをholdしたのと同じ状態になる。

3.4 begin_main,end_main

begin_main

<行>*

end_main

という形式でブロックを生成できる。このブロックにはシンボル"main"が「手続き名」として割り当てられる。

このとき

begin_defun

はオープンブロックジェネレータ、

end_defun

はクローズブロックジェネレータである。

マクロ表現をコンパイルして得られるコア表現をコアマシンに与えた場合、コアマシンは(プログラムファイルの先頭からでなく)"main"手続きの頭部から実行を開始する(かのようにジャンプで飛び込む)ようにコンパイルされる。

3.5 call命令

call <手続き名> <値の表現>*

の形式で、それより上方で定義されたか前方宣言された<手続き名>を割り当てられたブロックを実行することができる。

当該ブロックが「引数名」を持っていた場合、<値の表現>がそのシンボルにsaveされたうえで実行が始まる。

callは再帰的に呼び出されるかもしれない。即ちあるブロック内で自分自身を呼び出したり、前方宣言を利用して二つ以上の手続きが相互にお互いを呼び出し合うかもしれない。

あるブロック内でcallされたブロックは、呼び出し側のブロック内でholdされたレジスタを参照することも書き換えることもないことが保証されている。これは再帰的なcallが行われた場合も変わらない。すなわち再帰的なcallが発生した場合、hold宣言やbegin_defunの引数はシンボルにそのたびに新たなレジスタを割り付ける。

callの解決後には呼び出し前に割り付けられていたレジスタに割り付けが戻される。

3.6 前方宣言

 front_declare <関数名> <引数名>*

の形式は宣言文であり、前方宣言と呼ばれる。

前述したようにcall命令はそれ以降に定義された関数を呼び出せないため、そのままでは相互再帰的な呼び出し等はできない。

そこで前方宣言を呼び出したい位置より前に記述することで、「まだ定義されていない関数」を呼び出せる。

前方宣言した関数を実際に定義するときには以下の一点を除いて通常通りbegin_defun、end_defunを使えばよい。―即ち引数をもう一度宣言する必要はない。

ブロックジェネレータと同様、前方宣言もブロックの中で行われた場合の効果は未規定である。