【単純なスクリプトコンパイラを作る 第二部】
単純なスクリプトコンパイラの作ります。
念頭に置いた使用方法は、アドベンチャーゲームなどのシナリオデーターなどのスクリプトコンパイラです。
説明が余り無いので、ちょっとわかりにくいかもしれません。
●どういった事をしたいか
実用的なゲームでのシナリオのコンパイルを考えるため、どういった事をしたいか考えます。
1)行の終了は改行で。'(' や ')' などのカッコはなるべく使わない。
2)C言語風な代入式や比較式が使えること。
3)if - else - end 等の構造が作れること。
4)メッセージと式がそのまま平文でかけること。
5)メッセージ単位のセーブ/ロード、未読ポインタを管理できる仕様にすること。
●行の終了は改行で。'(' や ')' などのカッコはなるべく使わない。
シナリオライターのタイプ数をなるべく減らす為にこうします。
●C言語風な代入式や比較式が使えること。
この言語では、代入式と比較式の区別はつけません
極端に言うと、if 文で代入式をいれたり、何もないところで比較式を入れたり出来ます。
シナリオライターがスクリプト上でバグを発生させる原因になりますが、プログラマが慣れ親しんだC言語風の演算子を使うことが出来ます。
それと、選択肢の結果を入れる変数を指定するため、アドレス式という物を使用します。
なので、計2種類の式があることに注意して下さい。
それとC言語とは違い、全ての式を評価します。
(シナリオライターのバグの例 if r20==0 とするところを if r20=0 としてしまう )
●if - else - end 等の構造が作れること。
if文のブロック構造を考えるとき、if文自体を区切る方法とブロックを区切る方法とのの2パターンが存在します。
こうしないと、if 文の入れ子がある場合、曖昧さが出てきてしまいます。
たとえば、
if(式1) aaa if(式2) bbb else ccc
の場合、
if(式1) aaa { if(式2) bbb else ccc }
なのか
if(式1) aaa { if(式2) bbb } else ccc
なのかが不明瞭になります。
そこで、C言語では、各 else を else がなく一番近い if に対応付ける事にしています。
そこで、解決するために先ほどお話しした2パターンあり、この言語ではif文自体で区切るパターンを使います。
if文自体を区切る
if 式
elseif 式
else
end
ブロックを区切る
if 式 {
} else {
}
ちなみに、c言語では、if文はブロックを区切る方法、#ifプリプロセッサ命令は#if文自体で区切っています。
その他に、
while - end
do - end
switch - case - break - default - end
等が使用できるようにします。
●メッセージはそのまま平文でかけること。
以下の3パターンで書くことが出来るようにする。
[ボイスファイルネーム,]メッセージ
m [ボイスファイルネーム,]メッセージ
message [ボイスファイルネーム,]メッセージ
●メッセージ単位のセーブ/ロード、未読ポインタを管理できる仕様にすること。
a [ボイスファイルネーム,]メッセージ
add [ボイスファイルネーム,]メッセージ
式
c 式
calc 式
g cgファイルネーム
grph cgファイルネーム
s レジスタ番号(アドレス式),選択肢[,選択肢...]
sel レジスタ番号(アドレス式),選択肢[,選択肢...]
4)メッセージの未読ポインタを覚えたり、セーブロードの時のため、メッセージ単体にユニークな番号を振れること。
5)
ボイスファイルの番号は自動的に振らない
レジスタ固定
セーブの時とシナリオ追加の時のことを考え、関数やサブルーチンは無しとする
(関数/サブルーチンがあると、スタックも工夫してセーブする必要がある)
まずは文法を考えます。
大きく分けて、命令文、制御文、式の3つの要素を考えます。
●全く別の言語として、関数をつける
簡略化のために、関数をつけていないが、関数の実装方法を記する
なぜ、簡略化のために関数をつけていないかというと、関数の呼び出しには実アドレスがらみの問題が発生するからだ。
通常の製品版や本物のコンパイラを作る場合には関係ないが、バグfixでメッセージやローカル関数が追加された場合の処理が必要だからだ。
これをしないと、バグfixをするとセーブデータが使えなくなることがある。
スタックをつかい、関数内のローカルなレジスタを確保する。
パラメータの受け渡しや、関数内での使い捨てのレジスタのため。
言語使用を変更して、スタティックなレジスタ(r0-)以外に、ローカルなレジスタを確保できるようにする。
関数の実行中にセーブされると、スタック内のレジスタもセーブされる。
セーブロードのために、関数の呼び出しでは、アドレスだけではなく関数名も保存する。
コンパイル後のシナリオのヘッダーに関数名+アドレスの対応テーブルを作る。
それを使い、ロード後にロードされたスタックとシナリオのヘッダーを使い、スタックを再構築する。
これをしないと、a命令でシナリオをバグfixで追加するときにコンパイル後のアドレスとスタック上のアドレスがずれたままになる。
それと、セーブするときにはローカル変数もセーブされるのでやはりバグfixのときに手動で番号を振るためにローカル変数の追加のための命令が必要。
それと、関数の引数の追加をされたときに正常な動作をする仕組みも必要だ。
これらをひっくるめて、どこでもセーブ & バグfixの際に最大限の改変が出来るシステムにすると、以下の実行時に解決しなくてはならないことが生まれる。
1)現在のメッセージの位置を数えるシステム
2)バグfixの際の追加メッセージのための命令
3)関数名とアドレスの対応表
4)スタックのfixはスタックの中にフレームサイズを記入して、ロード時にチェックする
5)3)の表にローカルレジスタのサイズと引数のサイズも含める
6)バグfixの際に、ローカルレジスタの追加と引数の追加を出来るようにする
それらを考えると、いっそのことバグfixのときにはグローバルレジスタを使うようにして、引数の変更とローカルレジスタの追加は禁止にした方が良いかもしれない。
1)と2)は、関数がなくてもやった方がよい所だ。
ハッシュ
ハッシュは、関数の持たせ方によって3通りの方法がある。
1)c言語みたいに、グローバルと現在実行中の関数だけ参照する。上位関数内のラベルなどを参照できない
2)関数内に子関数を定義でき、子関数から親関数のラベルなどを参照できるようにする
3)ある関数から上位の関数内のラベルを参照できるようにする。
1)c言語みたいに、グローバルと現在実行中の関数だけ参照する。上位関数内のラベルなどを参照できない
シナリオがこんな感じだとすると、
main(){
printsub();
messub();
textsub();
}
printsub(){
loadfile();
}
messub(){
}
textsub(){
loadfile();
}
loadfile(){
}
ハッシュ表は
mainのハッシュ表 --+-- printsubのハッシュ表
|
+-- messubのハッシュ表
|
+-- textsubのハッシュ表
|
+-- loadfileのハッシュ表
loadfile関数からは 上位関数の printsub関数やtextsub関数を参照できない
そして、たとえばloadfile内でレジスタ名を解析する場合には、loadfileのハッシュ表 -> mainのハッシュ表の2つを解析する。
2)関数内に子関数を定義でき、子関数から親関数のラベルなどを参照できるようにする
シナリオがこんな感じだとすると、
main(){
printsub();
messub();
textsub();
}
printsub(){
loadfile(){
}
}
messub(){
}
textsub(){
loadfile(){
}
}
ハッシュ表は
mainのハッシュ表 --+-- printsubのハッシュ表 -- loadfileのハッシュ表
|
+-- messubのハッシュ表
|
+-- textsubのハッシュ表 -- loadfileのハッシュ表
3)ある関数から上位の関数内のラベルを参照できるようにする。
シナリオがこんな感じだとすると、
main(){
printsub();
messub();
textsub();
}
loadfile(){
r30=20;
}
printsub(){
int r30;
}
messub(){
}
textsub(){
int r30;
}
内部的には、2)と同じように printsub関数の下にも専用のloadfile関数があり、textsub関数の下にもそれ専用のloadfile関数がおのおのあるようにすると楽に実装できる。