「10日くらいでできる!プログラミング言語自作入門」の続編#1-7a
(1) HL-17a
- HL-17では、JITコンパイラ化のための下準備がメインで、命令としては単純代入とprint命令だけ対応しました。
- さすがにそれだけではさみしすぎるので、普通の演算子を全部JITコンパイラ化しようと思います。
- 改造の方針としては、いくつかのputIc()をputIcX64()に書き換えていくだけです。
#include <acl.c>
typedef unsigned char *String; // こう書くと String は unsigned char * の代用になる.
int loadText(String path, String t, int siz) → HL-4と同じなので省略
///////////////////////////////////////////////////////////////////////////////
#define MAX_TC 1000 // トークンコードの最大値.
String ts[MAX_TC + 1]; // トークンの内容(文字列)を記憶.
int tl[MAX_TC + 1]; // トークンの長さ.
unsigned char tcBuf[(MAX_TC + 1) * 10]; // トークン1つ当たり平均10バイトを想定.
int tcs = 0, tcb = 0;
AInt var[MAX_TC + 1]; // 変数.
int getTc(String s, int len) → HL-8aと同じなので省略
///////////////////////////////////////////////////////////////////////////////
int isAlphabetOrNumber(unsigned char c) → HL-2と同じなので省略
int lexer(String s, int tc[]) → HL-9aと同じなので省略
int tc[10000]; // トークンコード.
enum { TcSemi = 0, TcDot, TcWiCard, Tc0, Tc1, Tc2, Tc3, Tc4, Tc5, Tc6, Tc7, Tc8, TcBrOpn, TcBrCls, TcSqBrOpn, TcSqBrCls, TcCrBrOpn, TcCrBrCls,
TcEEq, TcNEq, TcLt, TcGe, TcLe, TcGt, TcPlus, TcMinus, TcAster, TcSlash, TcPerce, TcAnd, TcShr, TcPlPlus, TcEqu,
TcComma, TcExpr, TcExpr0, TcTmp0, TcTmp1, TcTmp2, TcTmp3, TcTmp4, TcTmp5, TcTmp6, TcTmp7, TcTmp8, TcTmp9,
TcSubPrint, TcSubTime, TcSubRgb8, TcSubXorShift, TcSubGetPix, TcSubF16Sin, TcSubF16Cos, TcSubInkey, TcSubPrints, TcSubSetPix0,
TcSubFilRct0, TcSubDrwStr0, TcSubGprDec, TcSubOpnWin, TcSubWait, TcSubBitBlt, TcSubAryNew, TcSubAryInit };
char tcInit[] = "; . !!* 0 1 2 3 4 5 6 7 8 ( ) [ ] { } == != < >= <= > + - * / % & >> ++ = , !!** !!*** _t0 _t1 _t2 _t3 _t4 _t5 _t6 _t7 _t8 _t9 "
"sub_print sub_time sub_aRgb8 sub_xorShift sub_aGetPix sub_f16Sin sub_f16Cos sub_aInkey sub_prints sub_aSetPix0 "
"sub_aFilRct0 sub_aDrwStr0 sub_gprDec sub_opnWin sub_aWait sub_bitblt sub_aryNew sub_aryInit";
///////////////////////////////////////////////////////////////////////////////
int phrCmp_tc[32 * 100], ppc1, wpc[9], wpc1[9]; // ppc1:一致したフレーズの次のトークンをさす, wpc[]:ワイルドカードのトークンの場所をさす.
int phrCmp(int pid, String phr, int pc) → HL-7と同じなので省略
/////////////////////////////////////////////////////////////////////////////// → ここまではHL-17と全く同じ、ここから改造が始まる.
#define ABI_MSW64 1 // Windows.
#define ABI_SYSV64 0 // Linuxなど.
typedef AInt *IntP; // こう書くと IntP は AInt * の代わりに使えるようになる.
enum { OpCpy = 0, OpCeq, OpCne, OpClt, OpCge, OpCle, OpCgt, OpAdd, OpSub, OpMul, OpDiv, OpMod, OpAnd, OpShr,
OpAdd1, OpNeg, OpGoto, OpJeq, OpJne, OpJlt, OpJge, OpJle, OpJgt, OpLop, OpPrint, OpTime, OpEnd,
OpPrints, OpAryNew, OpAryInit, OpArySet, OpAryGet, OpOpnWin, OpSetPix0, OpM64s, OpRgb8, OpWait,
OpXorShift, OpGetPix, OpFilRct0, OpPrm, OpF16Sin, OpF16Cos, OpInkey, OpDrwStr0, OpGprDec, OpBitBlt };
String opBin[] = { // 二項演算子のための機械語.
"%R_8b_%1m0; %R_3b_%2m0; 0f_94_c0; 0f_b6_c0; %R_89_%0m0;", // TcEEq ==
"%R_8b_%1m0; %R_3b_%2m0; 0f_95_c0; 0f_b6_c0; %R_89_%0m0;", // TcNEq !=
"%R_8b_%1m0; %R_3b_%2m0; 0f_9c_c0; 0f_b6_c0; %R_89_%0m0;", // TcLt <
"%R_8b_%1m0; %R_3b_%2m0; 0f_9d_c0; 0f_b6_c0; %R_89_%0m0;", // TcGe >=
"%R_8b_%1m0; %R_3b_%2m0; 0f_9e_c0; 0f_b6_c0; %R_89_%0m0;", // TcLe <=
"%R_8b_%1m0; %R_3b_%2m0; 0f_9f_c0; 0f_b6_c0; %R_89_%0m0;", // TcGt >
"%R_8b_%1m0; %R_03_%2m0; %R_89_%0m0;", // TcPlus +
"%R_8b_%1m0; %R_2b_%2m0; %R_89_%0m0;", // TcMinus -
"%R_8b_%1m0; %R_0f_af_%2m0; %R_89_%0m0;", // TcAster *
"%R_8b_%1m0; %R_99; %R_f7_%2m7; %R_89_%0m0;", // TcSlash /
"%R_8b_%1m0; %R_99; %R_f7_%2m7; %R_89_%0m2;", // TcPerce %
"%R_8b_%1m0; %R_23_%2m0; %R_89_%0m0;", // TcAnd &
"%R_8b_%1m0; %R_8b_%2m1; %R_d3_f8; %R_89_%0m0;" // TcShr >>
};
unsigned char *ic, *icq; // ic[]:内部コード、icq:ic[]への書き込み用ポインタ.
void putIc(int op, IntP p0, IntP p1, IntP p2, IntP p3) → HL-17と同じなので省略
int getHex(int c) → HL-17と同じなので省略
int get32(unsigned char *p) → HL-17と同じなので省略
void put32(unsigned char *p, int i) → HL-17と同じなので省略
void putIcX86_sub(String s, IntP a[]) → HL-17と同じなので省略
void putIcX86(String s, IntP p0, IntP p1, IntP p2, IntP p3) → HL-17と同じなので省略
///////////////////////////////////////////////////////////////////////////////
char tmp_flag[10]; // 一時変数の利用状況を管理.
int tmpAlloc() → HL-7と同じなので省略
void tmpFree(int i) → HL-7と同じなので省略
///////////////////////////////////////////////////////////////////////////////
void sub_print(int i) → HL-17と同じなので省略
void initTcSub() → HL-17と同じなので省略
///////////////////////////////////////////////////////////////////////////////
int epc, epc1; // exprのためのpcとpc1.
int exprSub(int priority); // exprSub1()が参照するので、プロトタイプ宣言.
int expr(int j);
int phrCmpPutIc(int pid, String phr, int pc, int *pi, int lenExpr, int op, int *err);
int phrCmpPutIcX64(int pid, String phr, int pc, int *pi, int lenExpr, void *sub, int *err); // この行を追加.
int exprSub1(int i, int priority, int op) // 二項演算子の処理の標準形.
{
int j, k;
epc++;
j = exprSub(priority);
k = tmpAlloc();
! putIcX64(opBin[op - TcEEq], &var[k], &var[i], &var[j], 0);
tmpFree(i);
tmpFree(j);
if (i < 0 || j < 0) return -1;
return k;
}
int exprSub(int priority)
{
int i = -1, e0 = 0, e1 = 0;
ppc1 = 0;
if (phrCmp(99, "( !!**0 )", epc)) { // かっこ.
i = expr(0);
} else if (tc[epc] == TcPlPlus) { // 前置インクリメント.
epc++;
i = exprSub(2);
! putIcX64("%R_8b_%0m0; %R_ff_c0; %R_89_%0m0;", &var[i], 0, 0, 0);
} else if (tc[epc] == TcMinus) { // 単項マイナス.
epc++;
e0 = exprSub(2);
i = tmpAlloc();
! putIcX64("%R_8b_%1m0; %R_f7_d8; %R_89_%0m0;", &var[i], &var[e0], 0, 0);
} else if (phrCmpPutIc(72, "mul64shr(!!**1, !!**2, !!**3)", epc, &i, 4, OpM64s, &e0)) {
} else if (phrCmpPutIc(73, "aRgb8(!!**1, !!**2, !!**3)", epc, &i, 4, OpRgb8, &e0)) {
} else if (phrCmpPutIc(74, "aOpenWin(!!**0, !!**1, !!***2, !!***8)", epc, 0, 3, OpOpnWin, &e0)) {
i = Tc0;
} else if (phrCmpPutIc(75, "aXorShift32()", epc, &i, 1, OpXorShift, &e0)) {
} else if (phrCmpPutIc(76, "aGetPix(!!**8, !!**1, !!**2)", epc, &i, 3, OpGetPix, &e0)) {
} else if (phrCmpPutIc(77, "ff16sin(!!**1)", epc, &i, 2, OpF16Sin, &e0)) {
} else if (phrCmpPutIc(78, "ff16cos(!!**1)", epc, &i, 2, OpF16Cos, &e0)) {
} else if (phrCmpPutIc(79, "aInkey(!!***8 , !!**1)", epc, &i, 2, OpInkey, &e0)) {
} else { // 変数もしくは定数.
i = tc[epc];
epc++;
}
if (ppc1 > 0)
epc = ppc1;
for (;;) {
tmpFree(e0);
tmpFree(e1);
if (i < 0 || e0 < 0 || e1 < 0) return -1; // ここまででエラーがあれば、処理を打ち切り.
if (epc >= epc1) break;
e0 = e1 = 0;
if (tc[epc] == TcPlPlus) { // 後置インクリメント.
epc++;
e0 = i;
i = tmpAlloc();
! putIcX64("%R_8b_%1m0; %R_89_%0m0; %R_ff_c0; %R_89_%1m0;", &var[i], &var[e0], 0, 0);
} else if (phrCmp(70, "[!!**0]=", epc) && priority >= 15) {
e1 = i;
e0 = expr(0);
epc = ppc1;
i = exprSub(15);
putIc(OpArySet, &var[e1], &var[e0], &var[i], 0);
} else if (phrCmp(71, "[!!**0]", epc)) {
e1 = i;
i = tmpAlloc();
e0 = expr(0);
putIc(OpAryGet, &var[e1], &var[e0], &var[i], 0);
epc = ppc1;
} else if (TcAster <= tc[epc] && tc[epc] <= TcPerce && priority >= 4) { // * / %
! i = exprSub1(i, 3, tc[epc]); // 左結合なので4より1小さくする.
} else if (TcPlus <= tc[epc] && tc[epc] <= TcMinus && priority >= 5) {
! i = exprSub1(i, 4, tc[epc]); // 左結合なので5より1小さくする.
} else if (tc[epc] == TcShr && priority >= 6) {
! i = exprSub1(i, 5, TcShr); // 左結合なので6より1小さくする.
} else if (TcLt <= tc[epc] && tc[epc] <= TcGt && priority >= 7) {
! i = exprSub1(i, 6, tc[epc]); // 左結合なので7より1小さくする.
} else if (TcEEq <= tc[epc] && tc[epc] <= TcNEq && priority >= 8) {
! i = exprSub1(i, 7, tc[epc]); // 左結合なので8より1小さくする.
} else if (tc[epc] == TcAnd && priority >= 9) {
! i = exprSub1(i, 8, TcAnd); // 左結合なので9より1小さくする.
} else if (tc[epc] == TcEqu && priority >= 15) {
epc++;
e0 = exprSub(15); // 右結合なので15のまま.
! putIcX64("%R_8b_%1m0; %R_89_%0m0;", &var[i], &var[e0], 0, 0);
} else
break;
}
return i;
}
int expr(int j) → HL-7と同じなので省略
///////////////////////////////////////////////////////////////////////////////
(中略)
///////////////////////////////////////////////////////////////////////////////
int compile(String s)
{
int pc, pc1, i, j;
unsigned char *icq1, *icp;
pc1 = lexer(s, tc);
tc[pc1++] = TcSemi; // 末尾に「;」を付け忘れることが多いので、付けてあげる.
tc[pc1] = tc[pc1 + 1] = tc[pc1 + 2] = tc[pc1 + 3] = TcDot; // エラー表示用のために末尾にピリオドを登録しておく.
icq = ic;
putIcX64("41_57; 41_56; 41_55; 41_54; 41_53; 41_52;", 0, 0, 0, 0);
putIcX64("41_51; 41_50; 57; 56; 55; 54; 53; 52; 51; 50;", 0, 0, 0, 0);
putIcX64("%R_81_ec_f8_01_00_00; %R_bd_%0q;", var, 0, 0, 0);
for (i = 0; i < 10; i++) {
tmp_flag[i] = 0;
}
tmpLabelNo = 0;
bd = lbd = 0;
for (pc = 0; pc < pc1; ) { // コンパイル開始.
int e0 = 0, e2 = 0;
if (phrCmp( 1, "!!*0 = !!*1;", pc)) { // 単純代入.
putIcX86("8b_%1m0; 89_%0m0;", &var[tc[wpc[0]]], &var[tc[wpc[1]]], 0, 0);
} else if (phrCmp(10, "!!*0 = !!*1 + 1; if (!!*2 < !!*3) goto !!*4;", pc) && tc[wpc[0]] == tc[wpc[1]] && tc[wpc[0]] == tc[wpc[2]]) {
putIc(OpLop, &var[tc[wpc[4]]], &var[tc[wpc[0]]], &var[tc[wpc[3]]], 0);
} else if (phrCmp( 9, "!!*0 = !!*1 + 1;", pc) && tc[wpc[0]] == tc[wpc[1]]) { // 高速化のために+1専用の命令を用意(せこくてすみません).
! putIcX64("%R_8b_%0m0; %R_ff_c0; %R_89_%0m0;", &var[tc[wpc[0]]], 0, 0, 0);
} else if (phrCmp( 2, "!!*0 = !!*1 !!*2 !!*3;", pc) && TcEEq <= tc[wpc[2]] && tc[wpc[2]] <= TcShr) { // 加算, 減算など.
! putIcX64(opBin[tc[wpc[2]] - TcEEq], &var[tc[wpc[0]]], &var[tc[wpc[1]]], &var[tc[wpc[3]]], 0);
} else if (phrCmpPutIcX86(4, "print !!**0;", pc, 0, 1, sub_print, &e0)) {
(中略)
}
int run(String s) → HL-17と同じなので省略
// 以下のmallocRWX()はWindows用の記述.
#include <windows.h>
void *mallocRWX(int siz) → HL-17と同じなので省略
///////////////////////////////////////////////////////////////////////////////
void aMain() → HL-17と同じなので省略
- プログラムは756行になりました。そして基本的な計算機能はすべて復活しました。
>print (1+2)*3
9
- もちろん他にも変数を使った演算なども自由にできますが、例を書いているときりがないので、ここでは省略します。
(2) 機械語の説明
- 8bや89については、もうすでに説明したので、ここでは説明を省略します。
- 表の中の%mは、mod r/mバイトを表しています。
%R_3b_%m | 比較命令。大小を比べる際には、まずこの命令を使って、どれとどれを比較するのかを明示する。 | アセンブラではCMP命令 |
0f_94_c0 ~ 0f_9f_c0 | 比較結果に応じて、RAXレジスタの下位8bitを0x00(偽)か0x01(真)にする。 | アセンブラではSETcc命令 |
0f_b6_c0 | RAXの上位56bitを0クリアする。 | アセンブラではMOVZX命令 |
%R_03_%m | 加算命令。 | アセンブラではADD命令 |
%R_2b_%m | 減算命令。 | アセンブラではSUB命令 |
%R_0f_af_%m | 符号付き乗算命令。 | アセンブラではIMUL命令 |
%R_99 | RAXをRDX:RAXに符号拡張する(IDIVの直前によく用いられる)。 | アセンブラではCDQ命令 |
%R_f7_%m/7 | 符号付き除算・剰余演算命令。商はRAX、剰余がRDXに入る。 | アセンブラではIDIV命令 |
%R_23_%m | AND計算(ビット演算)。 | アセンブラではAND命令 |
%R_d3_f8 | RAX = RAX >> RCX; を計算。 | アセンブラではSAR命令 |
%R_ff_c0 | RAX++; を計算。 | アセンブラではINC命令 |
%R_f7_d8 | RAX = - RAX; を計算。 | アセンブラではNEG命令 |
- 除算命令のところの「/7」の意味は、mod r/mバイトの中の第二オペランドに、演算対象のレジスタ番号を書くのではなく、そこは常に定数7を指定しなさい、という意味です。
- 除算命令は、割られる数は EDX:EAX で固定なので、mod r/m の第二オペランドでレジスタ番号を書く必要がないのです。そのかわりに、第二オペランドの部分も命令番号の一部にすることで、命令長の削減を狙っていると思われます。
- 参考までに、命令f7の/0~/7を全部書いておきます。
%R_f7_%m/0 | %R_f7_%m/1 | %R_f7_%m/2 | %R_f7_%m/3 | %R_f7_%m/4 | %R_f7_%m/5 | %R_f7_%m/6 | %R_f7_%m/7 |
TEST | 使用しない | NOT | NEG | MUL | IMUL | DIV | IDIV |
- 類似のテクニックは命令83や命令d3や命令c1でも見られます(c1はシフト命令の最適化のときに出てきます)。
次回に続く
こめんと欄