川合のプログラミング言語自作のためのテキスト第三版#9
(1) HL-9
- 今回は、HL-8aをaclライブラリを利用できるように書き換えた後で、いくつかのグラフィック命令を追加します。
- 追加の命令は以下の通りです。
タイプ | 形式 | 説明 | HL-9aのどのサンプルで使っているか? |
関数 | mul64shr(a, b, c) | a * b を64bitで計算して、結果を >> c したものを返す。 | mandel, kcube |
関数 | aRgb8(r, g, b) | 0~255のRGB値から、色番号を得る。 | mandel |
関数 | aOpenWin(sx, sy, t, dmy) | グラフィックウィンドウを開く。dmyは参照されない(同名のC関数の真似をしたかっただけ)。 | mandel, maze, kcube, invader |
関数 | aXorShift32() | 32bit符号付き整数の乱数を返す。 | maze |
関数 | aGetPix(win, x, y) | グラフィックウィンドウ上のピクセルの色番号を取得する。winは参照されない。 | maze |
関数 | ff16sin(t) | 角度は1周を65536度とする。そして結果は65536倍して、整数で返す。そういうsin。 | kcube |
関数 | ff16cos(t) | 角度は1周を65536度とする。そして結果は65536倍して、整数で返す。そういうcos。 | kcube |
関数 | aInkey(dmy, mod) | グラフィックウィンドウに入力されたキー入力を取得する。通常はmod=1で使う。 | invader |
命令 | aSetPix0(win, x, y, col) | グラフィックウィンドウの座標(x, y)のピクセルの色をcolに設定する。 | mandel |
命令 | aWait(msec) | 指定した秒数(ミリ秒)だけ待つ。 | mandel, maze, kcube, invader |
命令 | aFillRect0(win, xsiz, ysiz, x, y, c) | グラフィックウィンドウに塗りつぶした長方形を描画する。 | maze, kcube, invader |
命令 | aDrawStr0(win, x, y, col, bcol, str) | グラフィックウィンドウに文字列を描画する。 | invader |
命令 | gprintDec(win, x, y, len, col, bcol, i) | グラフィックウィンドウに整数iを描画する。 | invader |
命令 | bitblt(win, xsiz, ysiz, x, y, ary) | 配列ary[]に入ったカラーコード列を、指定された長方形範囲に転送する。 | invader |
- このうち、mul64shr()とff16sin()とff16cos()は、整数演算しかできないHL-9で、固定小数点演算を支援するためのものです。
- aclライブラリでは、intptr_tの代わりにAIntを使うのが標準的なので、書き換えています。
- aclライブラリでは、main()関数をvoid aMain()に書き換える必要があります。
- argcやargvはaArgc, aArgvで参照できます。
- aclライブラリは標準ライブラリのincludeを含むので、プログラム中でのincludeを省略できます。
#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-8aと同じなので省略
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 };
char tcInit[] = "; . !!* 0 1 2 3 4 5 6 7 8 ( ) [ ] { } == != < >= <= > + - * / % & >> ++ = , !!** !!*** _t0 _t1 _t2 _t3 _t4 _t5 _t6 _t7 _t8 _t9";
///////////////////////////////////////////////////////////////////////////////
int phrCmp_tc[32 * 100], ppc1, wpc[9], wpc1[9]; // ppc1:一致したフレーズの次のトークンをさす, wpc[]:ワイルドカードのトークンの場所をさす.
int phrCmp(int pid, String phr, int pc) → HL-7と同じなので省略
///////////////////////////////////////////////////////////////////////////////
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 };
IntP ic[10000], *icq; // ic[]:内部コード、icq:ic[]への書き込み用ポインタ.
void putIc(int op, IntP p0, IntP p1, IntP p2, IntP p3) → HL-6と同じなので省略
///////////////////////////////////////////////////////////////////////////////
char tmp_flag[10]; // 一時変数の利用状況を管理.
int tmpAlloc() → HL-7と同じなので省略
void tmpFree(int i) → HL-7と同じなので省略
///////////////////////////////////////////////////////////////////////////////
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 exprSub1(int i, int priority, int op) → HL-7と同じなので省略
int exprSub(int priority)
{
int i = -1, e0 = 0, e1 = 0;
ppc1 = 0;
(中略)
} else if (tc[epc] == TcMinus) { // 単項マイナス.
epc++;
e0 = exprSub(2);
i = tmpAlloc();
putIc(OpNeg, &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++;
}
(中略)
}
int expr(int j) → HL-7と同じなので省略
///////////////////////////////////////////////////////////////////////////////
enum { IfTrue = 0, IfFalse = 1 };
void ifgoto(int i, int not, int label) → HL-8と同じなので省略
int tmpLabelNo;
int tmpLabelAlloc() → HL-8と同じなので省略
#define BInfSiz 10
int binf[BInfSiz * 100], bd, lbd; // binf:block-info, bd:block-depth, lbd:loop-block-depth
enum { BlkIf = 1, BlkFor };
enum { IfLabel0 = 1, IfLabel1 };
enum { ForLopBgn = 1, ForCont, ForBrk, ForLbd0, ForWpc01, ForWpc11, ForWpc02, ForWpc12 };
int phrCmpPutIc(int pid, String phr, int pc, int *pi, int lenExpr, int op, int *err) // この関数はHL-9で追加したもの.
{
int e[9], i, i0 = 0;
if (phrCmp(pid, phr, pc)) {
e[0] = e[1] = e[2] = e[3] = e[4] = e[5] = e[6] = e[7] = e[8] = 0;
if (pi != 0) {
e[0] = *pi = tmpAlloc();
i0 = 1;
}
for (i = i0; i < lenExpr; i++) {
e[i] = expr(i);
}
putIc(op, &var[e[0]], &var[e[1]], &var[e[2]], &var[e[3]]);
if (lenExpr >= 5) {
putIc(OpPrm, &var[e[4]], &var[e[5]], &var[e[6]], &var[e[7]]);
}
for (i = i0; i < lenExpr; i++) {
if (e[i] < 0) {
*err = -1;
}
tmpFree(e[i]);
}
return 1;
}
return 0;
}
///////////////////////////////////////////////////////////////////////////////
int compile(String s)
{
(中略)
! } else if (phrCmpPutIc( 4, "print !!**0;", pc, 0, 1, OpPrint, &e0)) { // print.
(中略)
! } else if (phrCmpPutIc( 7, "time;", pc, 0, 0, OpTime, &e0)) {
(中略)
} else if (phrCmp(19, "if (!!**0) break;", pc) && lbd > 0) {
ifgoto(0, IfTrue, binf[lbd + ForBrk ]);
! } else if (phrCmpPutIc(20, "prints !!**0;", pc, 0, 1, OpPrints, &e0)) { // prints.
} else if (phrCmp(21, "int !!*0[!!**2];", pc)) {
e2 = expr(2);
putIc(OpAryNew, &var[tc[wpc[0]]], &var[e2], 0, 0);
} else if (phrCmp(22, "int !!*0[!!**2] = {", pc)) {
(中略)
+ } else if (phrCmpPutIc(23, "aSetPix0(!!***8, !!**0, !!**1, !!**2);", pc, 0, 3, OpSetPix0, &e0)) {
+ } else if (phrCmpPutIc(24, "aWait(!!**0);", pc, 0, 1, OpWait, &e0)) {
+ } else if (phrCmpPutIc(25, "aFillRect0(!!***8, !!**0, !!**1, !!**2, !!**3, !!**4);", pc, 0, 5, OpFilRct0, &e0)) {
+ } else if (phrCmpPutIc(26, "aDrawStr0(!!***8, !!**0, !!**1, !!**2, !!**3, !!**4);", pc, 0, 5, OpDrwStr0, &e0)) {
+ } else if (phrCmpPutIc(27, "gprintDec(!!***8, !!**0, !!**1, !!**2, !!**3, !!**4, !!**5);", pc, 0, 6, OpGprDec, &e0)) {
+ } else if (phrCmpPutIc(28, "bitblt(!!***8, !!**0, !!**1, !!**2, !!**3, !!**4);", pc, 0, 5, OpBitBlt, &e0)) {
} else if (phrCmp( 8, "!!***0;", pc)) { // これはかなりマッチしやすいので最後にする.
e0 = expr(0);
(中略)
}
AWindow *win; // この行を追加.
void exec()
{
clock_t t0 = clock();
IntP *icp = ic;
! AInt i, j, *a, sx, sy;
+ AInt32 *p32;
+ char s[100];
for (;;) {
switch ((int) icp[0]) {
(中略)
case OpEnd:
+ if (win != 0)
+ aFlushAll(win);
return;
(中略:ここの中のintptr_tもAIntに置換すること)
+ case OpOpnWin:
+ if (win != 0) {
+ if (win->xsiz < *icp[1] || win->ysiz < *icp[2]) {
+ printf("openWin error\n");
+ return;
+ }
+ } else
+ win = aOpenWin(*icp[1], *icp[2], (char *) *icp[3], 0);
+ icp += 5;
+ continue;
+ case OpSetPix0:
+ aSetPix0(win, *icp[1], *icp[2], *icp[3]);
+ icp += 5;
+ continue;
+ case OpM64s:
+ *icp[1] = (((AInt64) *icp[2]) * ((AInt64) *icp[3])) >> *icp[4];
+ icp += 5;
+ continue;
+ case OpRgb8:
+ *icp[1] = aRgb8(*icp[2], *icp[3], *icp[4]);
+ icp += 5;
+ continue;
+ case OpWait:
+ if (*icp[1] == -1) {
+ if (win != 0)
+ aFlushAll(win);
+ return;
+ }
+ aWait(*icp[1]);
+ icp += 5;
+ continue;
+ case OpXorShift:
+ *icp[1] = aXorShift32();
+ icp += 5;
+ continue;
+ case OpGetPix:
+ *icp[1] = aGetPix(win, *icp[2], *icp[3]);
+ icp += 5;
+ continue;
+ case OpFilRct0:
+ aFillRect0(win, *icp[1], *icp[2], *icp[3], *icp[4], *icp[6]);
+ icp += 10;
+ continue;
+ case OpF16Sin:
+ *icp[1] = (AInt) (sin(*icp[2] * (2 * 3.14159265358979323 / 65536)) * 65536);
+ icp += 5;
+ continue;
+ case OpF16Cos:
+ *icp[1] = (AInt) (cos(*icp[2] * (2 * 3.14159265358979323 / 65536)) * 65536);
+ icp += 5;
+ continue;
+ case OpInkey:
+ *icp[1] = aInkey(win, *icp[2]);
+ icp += 5;
+ continue;
+ case OpDrwStr0:
+ aDrawStr0(win, *icp[1], *icp[2], *icp[3], *icp[4], (char *) *icp[6]);
+ icp += 10;
+ continue;
+ case OpGprDec:
+ sprintf(s, "%*d", *icp[3], *icp[7]);
+ aDrawStr0(win, *icp[1], *icp[2], *icp[4], *icp[6], s);
+ icp += 10;
+ continue;
+ case OpBitBlt:
+ a = (AInt *) *icp[6];
+ p32 = &win->buf[*icp[3] + *icp[4] * win->xsiz];
+ sx = *icp[1];
+ sy = *icp[2];
+ for (j = 0; j < sy; j++) {
+ for (i = 0; i < sx; i++) {
+ p32[i] = a[i];
+ }
+ a += sx;
+ p32 += win->xsiz;
+ }
+ icp += 10;
+ continue;
}
}
}
int run(String s) → HL-6と同じなので省略
///////////////////////////////////////////////////////////////////////////////
void aMain() // 関数名を変更.
{
unsigned char txt[10000];
int i;
lexer(tcInit, tc);
! if (aArgc >= 2) {
! if (loadText((String) aArgv[1], txt, 10000) == 0) {
run(txt);
}
exit(0);
}
for (;;) { // Read-Eval-Print Loop.
(中略)
}
}
- プログラムは754行になりました。でもやっていることは、組み込み関数が増えただけです。
- 私としては、「組み込み関数を増やすのは簡単だ」ということと、「ちょっと組み込み関数を増やしてやるだけで、自作言語でできることはすごく増える」の二点を実感してほしいです。
- 組み込み関数を1つ増やすためには、enumに内部コード番号を一つ追加して、exprSub()かcompile()に1行追加して、exec()のcase文に処理内容を書くだけです。とてもシンプルです。
- このHL-9は例えば以下のプログラムを実行できます。
win = aOpenWin(256, 256, "gradation", 1);
for (y = 0; y < 256; y++) {
for (x = 0; x < 256; x++) {
aSetPix0(win, x, y, aRgb8(y, x, 0));
}
}
- これが動くのは当たり前だし当然なのですが、しかしそれでもこれがこんなに書いた通りに動いてくれると、「なんかまともになったなあ」と感じます。
- 他にも、mandelやmazeやkcubeやinvaderが動くのですが、今それをここで書くと、次のHL-9aと重複してしまって冗長なので、ここでは省略します。
(2) phrCmpPutIc()について
- HL-9を作り始めた時は、以下のような記述の繰り返しになってしまいました。
} else if (phrCmp(23, "aSetPix0(!!***8, !!**0, !!**1, !!**2);", pc)) {
e0 = expr(0);
e1 = expr(1);
e2 = expr(2);
putIc(OpSetPix0, &var[e0], &var[e1], &var[e2], 0);
} else if (phrCmp(24, "aWait(!!**0);", pc)) {
e0 = expr(0);
putIc(OpWait, &var[e0], 0, 0, 0);
} else if (phrCmp(25, "aFillRect0(!!***8, !!**0, !!**1, !!**2, !!**3, !!**4);", pc)) {
e0 = expr(0);
e1 = expr(1);
e2 = expr(2);
e3 = expr(3);
e4 = expr(4);
putIc(OpFilRct0, &var[e0], &var[e1], &var[e2], &var[e3]);
putIc(OpPrm, &var[e4], 0, 0, 0);
- こんな記述を繰り返すのは面倒で行数も食います。それでこれを短く書けるようにするために、phrCmpPutIc()という関数を作りました。
int phrCmpPutIc(int pid, String phr, int pc, int *pi, int lenExpr, int op, int *err)
{
int e[9], i, i0 = 0;
if (phrCmp(pid, phr, pc)) { // もしphrCmp()の結果が1だったら(=一致したら).
e[0] = e[1] = e[2] = e[3] = e[4] = e[5] = e[6] = e[7] = e[8] = 0;
if (pi != 0) {
e[0] = *pi = tmpAlloc(); // もしpiが0でなければ、e[0]にtmpAlloc()の結果を入れる.
i0 = 1;
}
for (i = i0; i < lenExpr; i++) { // e0 = expr(0); ~ に相当.
e[i] = expr(i);
}
putIc(op, &var[e[0]], &var[e[1]], &var[e[2]], &var[e[3]]); // putIc()で内部コードを出力.
if (lenExpr >= 5) { // e[4]以降も使われていたら、OpPrmを使って残りのパラメータを内部コードに記述.
putIc(OpPrm, &var[e[4]], &var[e[5]], &var[e[6]], &var[e[7]]);
}
for (i = i0; i < lenExpr; i++) { // e1~が負ではないことを確認、負なら*errで教える(pi==0なら、e0~)
if (e[i] < 0) {
*err = -1;
}
tmpFree(e[i]); // e1~をtmpFree()する.
}
return 1;
}
return 0;
}
- これがあれば、先の例は、
} else if (phrCmpPutIc(23, "aSetPix0(!!***8, !!**0, !!**1, !!**2);", pc, 0, 3, OpSetPix0, &e0)) {
} else if (phrCmpPutIc(24, "aWait(!!**0);", pc, 0, 1, OpWait, &e0)) {
} else if (phrCmpPutIc(25, "aFillRect0(!!***8, !!**0, !!**1, !!**2, !!**3, !!**4);", pc, 0, 5, OpFilRct0, &e0)) {
- のように、if文の条件成立節の中に何も書かなくていいほどシンプルになります。
(3) aclライブラリについて
- 普通、グラフィックスを使ったプログラムはOS依存があり、だからOSが変わったら再学習しなければいけません。でもこのライブラリはアプリがOSに依存しないで済む方法で作ってあるので、OSやCPUが違ってもプログラムは同じになります(コンパイル手順は変わります)。
- だからaclライブラリを使って作ったプログラムはそのまま他でも使えます。将来他のOSに乗り換えることになったとしても無駄になりません。
- このような性質を持つのは、なにもこのaclライブラリだけであるというわけではありませんが、その中でaclライブラリはかなり使いやすい部類であると私は思います。
- aclライブラリの関数は、たいていaで始まる名前が付けられています。
(4) aclライブラリの入手方法
- Windowsの場合: (Windows専用版を使う方法と、SDL2.0対応版を使う方法があります。)
- aclib21(Windows専用版をMinGW-5.1.6で使う例)
- aclib07(SDL2.0対応版をMinGW-5.1.6で使う例)
- MacOSの場合:
- Linuxの場合:
- Androidの場合: (Android 7.0以降が必要です。スマートフォンでもタブレットでもOKです。)
- aclib08 (SDL2.0対応版をTermux+clangで使う例)
- ラズベリーパイの場合:
- 「はりぼてOS」の場合:
- その他(SDL2.0とCの標準ライブラリを使った一般的なやり方):
(5) なぜHL-9ではaOpenWin()がいつも0を返して、aSetPix0()やaFillRect0()ではwinパラメータが無視される仕様にしたのか?
- 普通にwinのポインタを返す仕様にすることもできたのですが、HL-9で遊んでいると何かの拍子にwinに(=aOpenWin()の返値を入れておいた変数に)適当な値を入れて上書きしてしまうことがあり、こうなってしまうとwinに対する操作が何もできなくなってしまいます。
- それならば、いっそのことwinの値はHL-9が内部で管理しておくことにして、ユーザには本当の値を教えないようにしました。そして描画関数が呼ばれた場合は、ユーザが指定したwinの値は無視して、HL-9内部で管理された値を使うようにしています。
- なんとなく思いつきでやってみただけです。あまり深い意味はありません。
次回に続く
こめんと欄