* 「10日くらいでできる!プログラミング言語自作入門」の続編#1-7a -(by [[K]], 2021.05.08) ** (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; %R_0f_b6_c0; %R_89_%0m0;", // TcEEq == "%R_8b_%1m0; %R_3b_%2m0; 0f_95_c0; %R_0f_b6_c0; %R_89_%0m0;", // TcNEq != "%R_8b_%1m0; %R_3b_%2m0; 0f_9c_c0; %R_0f_b6_c0; %R_89_%0m0;", // TcLt < "%R_8b_%1m0; %R_3b_%2m0; 0f_9d_c0; %R_0f_b6_c0; %R_89_%0m0;", // TcGe >= "%R_8b_%1m0; %R_3b_%2m0; 0f_9e_c0; %R_0f_b6_c0; %R_89_%0m0;", // TcLe <= "%R_8b_%1m0; %R_3b_%2m0; 0f_9f_c0; %R_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(); ! putIcX86(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と同じなので省略 -プログラムは760行になりました。そして基本的な計算機能はすべて復活しました。 >print (1+2)*3 9 -もちろん他にも変数を使った演算なども自由にできますが、例を書いているときりがないので、ここでは省略します。 -(以下編集中) ** (2) 機械語の説明 -8bや89については、もうすでに説明したので、ここでは説明を省略します。 -表の中の%mは、mod r/mバイトを表しています。 |3b_%m|比較命令。大小を比べる際には、まずこの命令を使って、どれとどれを比較するのかを明示する。|アセンブラではCMP命令| |0f_94_c0 ~ 0f_9f_c0|比較結果に応じて、EAXレジスタの下位8bitを0x00(偽)か0x01(真)にする。|アセンブラではSETcc命令| |0f_b6_c0|EAXの上位24bitを0クリアする。|アセンブラではMOVZX命令| |03_%m|加算命令。|アセンブラではADD命令| |2b_%m|減算命令。|アセンブラではSUB命令| |0f_af_%m|符号付き乗算命令。|アセンブラではIMUL命令| |99|EAXをEDX:EAXに符号拡張する(IDIVの直前によく用いられる)。|アセンブラではCDQ命令| |f7_%m/7|符号付き除算・剰余演算命令。商はEAX、剰余がEDXに入る。|アセンブラではIDIV命令| |23_%m|AND計算(ビット演算)。|アセンブラではAND命令| |d3_f8|EAX = EAX >> ECX; を計算。|アセンブラではSAR命令| |40|EAX++; を計算。|アセンブラではINC命令| |f7_d8|EAX = - EAX; を計算。|アセンブラではNEG命令| --除算命令のところの「/7」の意味は、mod r/mバイトの中の第二オペランドに、演算対象のレジスタ番号を書くのではなく、そこは常に定数7を指定しなさい、という意味です。 --除算命令は、割られる数は EDX:EAX で固定なので、mod r/m の第二オペランドでレジスタ番号を書く必要がないのです。そのかわりに、第二オペランドの部分も命令番号の一部にすることで、命令長の削減を狙っていると思われます。 --参考までに、命令f7の/0~/7を全部書いておきます。 |f7_%m/0|f7_%m/1|f7_%m/2|f7_%m/3|f7_%m/4|f7_%m/5|f7_%m/6|f7_%m/7| |TEST|使用しない|NOT|NEG|MUL|IMUL|DIV|IDIV| --類似のテクニックは命令83や命令d3や命令c1でも見られます(c1はシフト命令の最適化のときに出てきます)。 ** 次回に続く -次回: [[a21_txt02_2]] *こめんと欄 #comment