acl4の開発ログ
- 最近の過去ログ:
- a4_log09(2026.03.18(水)#0~2026.03.26(木)#5): 有理数型を作ろうと思ったけど、途中から浮動小数点数から近い有理数を探すアルゴリズムに注目。
- a4_log10(2026.03.27(金)#0~2026.04.10(金)#3): プリプロセッサの連鎖でコンパイラを構成するやり方を引き続き試作。
- a4_log11(2026.04.11(土)#0~2026.04.18(土)#0): acl4 → acl4v1 の移行。
2026.04.20(月) #0
- mallocでメモリを確保したり、reallocでメモリを拡張したりすると、なんか適当な値が雑に書かれたメモリが手に入ります。そのことそのものに問題はないのですが、しかしデバッグするときにこれを見て「初期化をしてないからこの値なのか」「初期化をしたからこの値なのか」よくわからない気分になります。だから自作mallocと自作reallocに手を加えてデバッグモードの時はメモリを適当な値で塗りつぶして渡すという仕組みを作ることにしました。
- とりあえず簡単のためにmemset()で塗りつぶすことにして、ではどんな値で塗りつぶすかということを考えました。
- 0x00: これで塗りつぶすと明らかに「何も書いてない」感じになります。それはとても良いですが、そもそもプログラムのほうで初期化する際も 0x00 で塗りつぶすことが多く、つまりこれだと初期化を忘れてもかえって気づきにくい、初期化し忘れているのにデバッグモードだと正常に動いちゃう、という問題があります(リリースモードでは動かない)。0x00にしたいのならプログラムが自分でやるべきです。mallocやreallocを当てにされては困ります。
- 0xff: 0x00が一番きれいなのだとしたら、じゃあ一番汚いのは何か?それは0xffだよね、と考えました。数値的にも255は0から一番遠い値で、未初期化だと最も気づきやすそうです。・・・しかし、非常によく使われる符号付き整数で考えれば、0xffは8bitの-1でもあります。0xffffは16bitの-1、0xffffffffは32bitの-1です。つまり未初期化で値を取り出すと-1が読めてしまうわけです。-1なんて0にかなり近い数値です。初期化を忘れていても、「なんかちょっとだけおかしいかもしれないけど、まあでもこんなものかな?」で見過ごされる可能性があります。だからもっと明らかにおかしい値にするべきです。
- 0x80: では符号付きの整数で、0x00から最も遠い値にしたらどうかと考えました。8bitの0x80は-128、16bitの0x8080は-32640、32bitの0x80808080は-2139062144です。これはちょっと普通では出てこない数値でしょう。しかし、だからこそ、invalidな値として初期値で使われる場合もあります(私自身もやったことがあります)。・・・それだと初期化忘れを見逃す原因にもなりうるのです。
- 0x87: それでは、この1バイトの中の「0」のビットと「1」のビットを同数にすることにして、それで0x00から最も遠い値というのを考えてみました。「0のビットと1のビットを同数にする」というのは、メモリチェックパターンの0x55aaとかでも使われている考え方です。今回はメモリチェックとは全く関係ない文脈ですが、オールゼロやオールワンが真っ先にダメだというのなら、いっそのこと半々にしてしまえ、という安易な発想です。・・・0x87や0x8787などを初期値として採用するケースはまず考えられないですし、異常値っぽさは十分にあります。ということで今回はこれで行くことにします。
2026.04.20(月) #1
- acl4v1でもdefine機能が使えるようになりました。でもまだtypedDefがうまく動いていません。
2026.04.20(月) #2
- なんか最近このwikiが不安定な気がします。よしxreaからcoreserverに移行しよう!
2026.04.21(火) #0
2026.04.21(火) #1
- 移行できたと思います。無事に安定して表示できるといいなあ。
2026.04.21(火) #2
- さっきバグって、ポインタが変な値になっていました。当然、セグメンテーションフォルトで落ちたわけですが、なんでだ?と思ってポインタ値をprintfしてみたら、なんと 0x87878787 !
- ・・・うお、いきなりこれかー(笑)。つまり初期化してないポインタがあって、そこを読んだわけですね。なんてわかりやすい!
- ということで30秒くらい、くくく・・・と笑ってしまいました。
- [註] 2026.04.20(月)#0 で自分が仕掛けたバグ発見用のトラップに、さっそくはまって原因が分かった、という事例です(笑)。
- いや、これに限らず、私は毎日何度も自分の仕込んだデバッグトラップを踏んでいます。
- まあなんとも情けないわけですが、でもそのうちの多くはデバッグトラップがなくてもすぐに直せただろうとは思います。
- ただなんというか、私のデバッグトラップたちが優秀すぎて(?)、私が頑張る前に先にバグを見つけてしまうんです。
- ありがたい!! acl4 は私に合っている!!
2026.04.22(水) #0
- a4_0001~a4_0014のリファクタリングが完了しました。次はミニコンパイラをacl4v1用に直します。
2026.04.24(金) #0
- やっと、何を作ったらプリプロセッサの良さが伝わるかが分かったので、頑張って作ります。たぶん明日か明後日にはできます!
2026.04.25(土) #0
- できました!
- a4_p0007: プリプロセッサ処理をする関数を作ったら、いろんな言語が簡単に作れるかもしれない可能性が出てきました!
- a4_i01のページを改装中です。
2026.04.26(日) #0
2026.04.27(月) #0
- a4_p0007のページにダウンロード用のzipファイルを追加しました。これで、35.0KBってホントにできるの?って言われずに済みます。
- x86-32でコンパイルしないと、35.0KBにはなりません。あと /Da_DbgLv=0 をつけて、リリースモードにする必要もあります。
- さて、次にやるべきは配列変数のサポートです。配列変数が使えるようになれば、kcubeが動かせるようになるはずです。
- とにかくこの「プリプロセッサで置換する方式」でどこまで行けるのか確かめたいです。
2026.04.28(火) #0
- a4_t0002 は Windows 専用になっていますが、かつて X11 でのグラフィックのやり方を勉強したので、それを思い出せば、 Linux 版も作れそうだとは思っています。
2026.04.30(木) #0
- 考えがうまくまとまらないので、とりあえずシンプルにやります。
- たぶん設計がまとまらないのは、それを設計するだけの知識が揃っていないのだと思います。そんなときに無理しても駄作になって結局作り直すことになるので、背伸びはせず、単純明快にできるところまでをやります。
2026.05.01(金) #0
- A4vm_eval0()のEnter命令の仕様:
- Ent_III(i, j, k)
- i: 整数レジスタ数, j: 浮動小数点数レジスタ数, k: 関数呼び出しのための予約メモリ
- 今回追加する引数:
- Ent_IIIII(i, j, k, l, m)
- l: ポインタレジスタ数, m: スタック変数のための領域サイズ
2026.05.01(金) #1
- そうかー! 構造体をどうしたらいいかなって考えるよりも先に、プリプロセッサの力でJITコンパイラが作れるのか試すべきです。
- もしうまくできたら、「これでもう将来どんなCPUが主流になっても、簡単に移植できるよー」って言えるようになるはずです。
2026.05.01(金) #2
2026.05.01(金) #3
2026.05.01(金) #4
- まだ printf しかできないですが、とにかくJITコンパイラはできました。ちゃんとx86の機械語に変換されて動いています。
- [追記] その後、for文もできました。簡単すぎて楽しいです。
- [追記] さらに1時間くらいやったら、 p0007h.txt が動くくらいになりました!( mandel )。やっぱりJITコンパイラは実行が速いです。
- やっぱりプリプロセッサは偉大だなあー。
2026.05.03(日) #0
- 今、 p0008a.c っていうJITコンパイラを作っているのですが、その中身は Cコンパイラ+アセンブラ になっています。
- そのアセンブラ部分は、 #define RET() DB(0xC3) みたいなのを50行くらい書いただけでしかないのですが、命令によってはこのDB命令は連続するので DB(a, b); みたいな形式もサポートしたいなって思いました。
- でもなあ、複数の引数をサポートするのは面倒だよなあ、まあだから現状のままでいいかー、2回書けばいいだけだし・・・とか思って妥協していました。
- でもさっき、思いつきました。あれ?もしかして、 #define DB(a, b) DB(a); DB(b) っていう1行を追加しておけばいいだけなんじゃないの?
- その通りでした。こうして DB() 命令は複数の引数に対応できたのでした。
- こんなに楽していいのかな?(笑)
- 上記で、平気なふりして「#define を50行くらい書くだけで、アセンブラができた」みたいなことを書いていますが、これも結構とんでもなくて、今まで何度かアセンブラを作ってきた私としては「今までの苦労は一体なんだったのか・・・」という気持ちになっています。
- まあ進歩ってそういうものですよね。
2026.05.04(月) #0
2026.05.04(月) #1
- a4_t0003 を書きました。現行のミニコンパイラを補助ライブラリとしてまとめて、何度もコピー&ペーストしないでもいいようにしました。
2026.05.04(月) #2
2026.05.06(水) #0
- 「新しい言語の作り方」という12ページのスライド資料をなんとなく作りました。
2026.05.10(日) #0
- 運よくプレゼン機会があったので、上記の「新しい言語の作り方」でLTをしてきました(昨日)。
- 好評でたくさんコメントをもらえたので、それを加筆したバージョンを作ろうと思います。
2026.05.13(水) #0
- [1] 第1層(a4_0016)
- デバッグに便利な機能が付いた malloc/realloc/free を整備。
- [2] 第2層(a4_0017~a4_0019)
- 第1層の成果を使って、vector<char> を参考にして作った VecChr クラス(動的可変長配列)、簡単な Key-Value Store を提供する Set0 クラスを整備。
- さらにトークンを切り出す Token0/Token1 クラス、かっこでくくられた構造を切り分ける parseArgs などを整備。
- [3] 第3層(a4_0020)
- 第2層の成果を使って、式の評価クラスを整備。文字列で式を与えれば、計算して答えを返します。
- [4] 第4層(a4_0021~a4_0022)
- 第3層までの成果を使って、プリプロセッサ関数を整備。
- [5] 第5層(a4_0023~)
- ということで、まずAを作り、そのAを利用してBを作り、そのBを利用してCを作り・・・というのを繰り返して、どんどん自分の開発力をアップさせようというたくらみは、今のところ順調に進んでいます!
2026.05.16(土) #0
- 何となく自分が納得できる程度の memmem() ができたのでここに書きます。
- まず memmem() が何かというと strstr() の仲間みたいなものです。ゼロ終端文字列ではなくて、メモリブロックの中からメモリブロックを探すのです。
- GNU拡張なので、標準の <string.h> には入っていません。私はとっても便利な関数だと思っていますが。
- 普通に作っても面白くないので、ちょっとだけ工夫をして高速化してあります。
- 説明用のバージョン:
a_static char *a_memmem0(const char *t, intptr_t tn, const char *p, intptr_t pn)
{
if (pn <= 0) return (char *) t;
if (tn < pn) return NULL;
intptr_t i = pn - 1;
const char *t1 = t + tn - pn, c0 = p[0];
for (t--;;) {
nxt:
t = memchr(t + 1, c0, t1 - t);
if (t == NULL) return NULL;
if (t[i] == p[i]) {
for (i = 1; i < pn; i++) {
if (t[i] != p[i]) goto nxt;
}
return (char *) t;
}
}
}
- まず memchr() で0バイト目が一致している場所を見つけてきます。無事に見つかったら、i番目についても比較します。それも一致したら、全部比較します。
- 全部比較するときに memcmp() を使えばいいし、比較だけならたぶんそのほうが速いのですが、不一致だった場合に何バイト目で食い違ったのかを知りたいので、自前ループしています。
- 普通、この手のストリング検索では、 Boyer–Moore の有名なアルゴリズムを使って高速化します。しかしそれは事前解析処理が面倒だったので今回はやらないで、不一致の場合に速く気付くことを目指してこのアルゴリズムを思いつきました。まあシンプルなので、たぶん車輪の再発明だろうと思います(笑)。
- 最初に t--; して、次に t+1 から memchr() するのは一見すると無駄なのですが、不一致になって戻ってきたときに t+1 してくれるのは便利なのです。だから最初に t--; してでも、この構造にしたかったのです。
- でもこのバージョンをそのまま使うと、 memchr() があまり速くありません。こういうストリング系の関数は自前実装しても性能がよくなることはほとんどないと言われていますが、私はそんなうわさ程度じゃあきらめません。そもそもここに書くことになったということは、自前実装でも十分に速くなったから書いているのです(笑)。
- ちゃんと高速化したバージョン:
a_static char *a_memmem(const char *t, intptr_t tn, const char *p, intptr_t pn)
{
if (pn <= 0) return (char *) t;
if (tn < pn) return NULL;
intptr_t i = pn - 1;
const char *t1 = t + tn - pn, *t16 = t1 - 16, c0 = p[0];
for (t--;;) {
nxt:
t++;
for (;;) {
if (t <= t16) {
if (t[0] == c0) goto t0;
if (t[1] == c0) goto t1;
if (t[2] == c0) goto t2;
if (t[3] == c0) goto t3;
if (t[4] == c0) goto t4;
if (t[5] == c0) goto t5;
if (t[6] == c0) goto t6;
if (t[7] == c0) goto t7;
if (t[8] == c0) goto t8;
if (t[9] == c0) goto t9;
if (t[10] == c0) goto t10;
if (t[11] == c0) goto t11;
if (t[12] == c0) goto t12;
if (t[13] == c0) goto t13;
if (t[14] == c0) goto t14;
if (t[15] == c0) goto t15;
t += 16;
continue;
}
if (t > t1) return NULL;
if (t[0] == c0) goto t0;
t++;
}
t15: t++;
t14: t++;
t13: t++;
t12: t += 12; goto t0;
t11: t++;
t10: t++;
t9: t++;
t8: t += 8; goto t0;
t7: t++;
t6: t++;
t5: t++;
t4: t += 4; goto t0;
t3: t++;
t2: t++;
t1: t++;
t0:
if (t[i] == p[i]) {
for (i = 1; i < pn; i++) {
if (t[i] != p[i]) goto nxt;
}
return (char *) t;
}
}
}
- さあ、久しぶりに「キモイい」プログラムになりました。
- この改造の趣旨は、とにかくtを加算したり、tnとの比較をしたりする頻度を下げたかったのです。だから16回に1度だけ加算&比較するようにしました。
- それで一致した場所が見つかった場合は、tをその分だけ加算しなければつじつまがあわないので、t++;が連続しているところのどこかへ分岐させています。
- やっていることはそれだけです。
- 芸のないベンチマーク:
#include <acl4v1.c>
#include "t0003a.c" // ここにa_memmem()があります.
int main(void)
{
char *s = malloc(900 * 1024 * 1024); // 900MB.
int i, j = 0;
for (i = 0; i <= 100000000; i++) // とりあえず1億.
j += sprintf(s + j, "%d ", i);
printf("j=%d\n", j);
clock_t t0, t1, t2;
t0 = clock();
int rs = (int) strstr(s, " 100000000");
t1 = clock();
int rm = (int) a_memmem(s, j, " 100000000", 10);
t2 = clock();
printf("strstr=%x\n", rs);
printf("a_memmem=%x\n", rm);
printf("t1-t0=%d t2-t1=%d\n", t1-t0, t2-t1);
return 0;
}
- 次に、 a_memmem() が少し有利になりそうなベンチマークです。いつも最初の数文字が一致しているようなパターンです。
#include <acl4v1.c>
#include "t0003a.c"
int main(void)
{
char *s = malloc(140 * 1024 * 1024); // 140MB.
int i, j = 0;
for (i = 0; i <= 10000000; i++) // とりあえず100万.
j += sprintf(s + j, "count=%d ", i);
printf("j=%d\n", j);
clock_t t0, t1, t2;
t0 = clock();
int rs = (int) strstr(s, "count=10000000");
t1 = clock();
int rm = (int) a_memmem(s, j, "count=10000000", 14);
t2 = clock();
printf("strstr=%x\n", rs);
printf("a_memmem=%x\n", rm);
printf("t1-t0=%d t2-t1=%d\n", t1 - t0, t2 - t1);
return 0;
}
2026.05.22(金) #0
- 最近は、SecHack365の選考に忙殺されていて、acl4があまり進んでいません。
- あ、でも、昨日は buntan-pc のuchanさんと、CPUにこんな命令があったら便利になるかもしれないっていう話をたくさんしました。私は CPU の自作を気軽にやったりはできないのですが(勉強不足で)、仮想マシンとしてそういう命令を持たせることなら余裕でできるので、いつか試すかもしれません。
- 考えていた内容は、たとえば整数演算でゼロ割しても例外を起こさずに商を 0x800...000 として処理を続行してくれて、またNULLポインタでメモリライトしても無視してくれて、NULLポインタでメモリリードしたら 0x800...000 が読みだされて続行されるような、そんな命令体系です。どの場合も、問題が起きて対処した場合はフラグレジスタ内のエラーフラグを1にするくらいはしてもいいかなと思っています。加減算などでもオーバーフローを検出したら値を 0x800...000 にするモード(モードというか新設命令)があってもいいと思っています。
- この命令体系だとプログラミングのパラダイムが変わるかもしれなくて、今までは演算する前に入力値が正常の範囲に収まっているかチェックしながら演算してきました。そうでないと例外を踏まされるからです。でもこの命令体系なら、値はおかしくなるもののとにかく処理は問題なく続行できるので、最後にエラーフラグをみてエラー処理するだけで済みます。結果的にif文が減ると思うので高速化するかもしれません。
2026.05.26(火) #0
- 4bitあれば 0000~1111 の16通りの表現があります。これをAパターン(a通り)とBパターン(b通り)に分けます。a+b=16とします。
- Bパターンが来たら、そこで終了です。これで 0~(b-1) を表現できます。
- Aパターンが来たら、Bパターンが来るまで繰り返してから終了します。ABの場合 0~(ab-1)を表現できます。AAAB形式なら 0~(aaab-1) です。
- hh4の単純形式の場合、a=b=8になっていて、だから 8/64/512...と拡張していくことができます。
- 拡張形式として、10+6=16 みたいなものを考えることができます。4bitで0~5、8bitで0~59、12bitで0~599、16bitで0~5999が表現できます。8+8=16と比べると、4bit形式は7→5、8bit形式は63→59と犠牲になりますが、12bitは511→599となって少し増えます。
- 表にしてみます。
| 8+8=16型 | 8 | 64 | 512 | 4096 | | 10+6=16型 | 6 | 60 | 600 | 6000 | | 7+9=16型 | 9 | 63 | 441 | 3087 | | 6+10=16型 | 10 | 60 | 360 | 2160 | | 5+11=16型 | 11 | 55 | 275 | 1375 | | 4+12=16型 | 12 | 48 | 192 | 768 |
- この中だと4+12=16はかなり魅力的に見えます。
2026.05.27(水) #0
- a26_txt03(川合のプログラミング言語自作のためのテキスト第四版#0)を書き始めました。
2026.05.30(土) #0
- 説明を平易にしようとすると、ポインタとかはあまり使えないよなあと思いつつ、でもやりたいことができないのは本末転倒なので、やっぱりポインタを遠慮なく使って書いています。
- だから言い訳を考えているのですが、結局ライブラリは「どんな機能を提供してくれているのか」が一番大事で、その実装の細部まで理解しなきゃいけないかというとそんなことはないですよね。・・・「私たちは printf() の実装がどうなっているか知らなくても、 printf() を問題なく使えるのです。だから分かるところまでわかれば良くて、よくわかんないところは適当に読み飛ばしてください」という説明で逃げようかなと・・・。
2026.05.31(日) #0
2026.06.01(月) #0
- メモリ管理とかデバイス管理など、プログラミングでは〇〇管理という機能を作ることがよくあります。それでその管理を担うクラスとかは管理者なので、〇〇マネジャーという名前で命名します。その際に私は長い名前を嫌うので、マネジャーを短くしようとして Man と略してきました。だからメモリ管理なら MemMan になります。
- ところが、ふと「なぜ私は Manager を Man と略すのだろうか」と思いました。つまり Mgr じゃないのはなぜか?ということです。・・・いや、私の漠然とした認識では、「人間のマネージャーとかを略して書くときは Mgr だけど、プログラミングの世界では Man と書く」ということになっていたのですが、果たして本当にそうなのかと急に不安になったのです。
- 不安は的中します。Google検索で調べていたらGeminiに、プログラミングの世界でも普通はMgrだよ、むしろManはマニュアルの略だよ!Manだと男の人とも間違えやすいからよくないよ、と言われてしまいました。・・・しかし一方で「DevMan」は何の略だと思うかとGeminiに聞くと、「デバイスマネジャー」か「デベロップメントマネジャー」だと言われました。あれ?キミはそこは「デバイスマニュアル」と言うところじゃないの??(笑)。
- Geminiのちょっと微妙な受け答えはともかくとして、Man派は少数でありそうな感じはしました。よしこれからは私も Mgr 派になろう!
- とまあ、それはそれでよいとして、ではなぜ私は今まで Man 派でここまでやってきたのかという疑問が残ります。私は自分で省略形を勝手に考え出したわけではないのです。・・・それで、私が最初にプログラミングで〇〇マネジャーというのを学んだときに省略形が Man だったから、それに引きずられてきたんだろうと考えました。
- 私が最初に学んだのは・・・OS-9/6809です。それで昔読んだ書籍をひっぱり出して調べてみたら、 IOMAN, RBFMAN, SCFMAN, PIPEMAN, SBFMAN, NFMAN などと、それらしきものがどんどん見つかりました。ああこれです。私はこれを最初に見たから Man=マネジャー になったのです。そっかー、OS-9は主流になれなかったから、この略し方はそれ以上には流行らなかったということなんですね。納得しました。
2026.06.01(月) #0
- a26_txt03_001(川合のプログラミング言語自作のためのテキスト第四版#001)を書きました。
- このログページの一部を切り出してa4_log11を作って、その分このログページを短くしました(長すぎだよなーと思っていたので)。
2026.06.02(火) #0
- 「デバッグ支援機能はこれが最後です」なんて書いたら、「うーん他に何かいいアイデアはないかな、あったら今のうちに入れておかないと・・・」っていう気分になってきました(笑)。
2026.06.09(火) #0
- ここしばらく、SecHack365関係で気が散って、あまり進んでいませんでしたが、そろそろ再開します!
2026.06.10(水) #0
- C言語の標準ライブラリのqsort()関数は、配列をソートできて便利なのですが、 a[i]があったとして、私が欲しいのはa[i]の最大値や最小値ではなく、いや最大値や最小値も欲しいのですが、それよりもその時のiがいくつ七日が知りたいです。つまりargmaxとかargminです。もっというと1番目だけじゃなくて2番目や3番目のiも知りたいです。
- この目的のためにqsortを使う場合は、a[i]の値とiの値を構造体で組にしてソートするのが普通かなと思いますが、もういっそのこと、iだけにしてしまってソートしてしまえばいいと思ってやってみました。
int (*iqsor_cmpF)(void *, int, int);
void *iqsor_cmpW;
int iqsort_cmp(const void *a, const void *b)
{
int ai = *(const int *) a;
int bi = *(const int *) b;
return iqsor_cmpF(iqsor_cmpW, ai, bi);
}
void iqsort(int *a, int n, int (*cmpF)(void *, int, int), void *cmpW)
{
int i;
for (i = 0; i < n; i++)
a[i] = i;
iqsor_cmpF = cmpF;
iqsor_cmpW = cmpW;
qsort(a, n, sizeof (int), iqsort_cmp);
}
// ここからサンプルコード.
int cmp(void *w, int i, int j)
{
const char **s = w;
return strcmp(s[i], s[j]);
}
int main()
{
char *name[5] = { "Tanaka", "Yamada", "Kawai", "Sato", "Suzuki" };
int idx[5], i;
iqsort(idx, 5, cmp, name); // これで idx[] を得る.
for (i = 0; i < 5; i++) { printf("%d ", idx[i]); } printf("\n");
for (i = 0; i < 5; i++) { printf("%d: %s\n", i, name[idx[i]]); }
return 0;
}
- これを実行すると、以下のようになります。
2 3 4 0 1
0: Kawai
1: Sato
2: Suzuki
3: Tanaka
4: Yamada
- 最初にidx[i]の値が表示されています。idx[0]は2なので、一番小さいのはname[2]です。idx[4]は1なので、一番大きいのはname[1]です。
- 要点は、name[]は全く並べ替えられることなくそのままになっていて、idx[]に順番を辞書引きするための目次(index)が入っているというところです。・・・あとは比較関数に任意のポインタを一つ渡せるようにしています。
- 私はこの仕様のほうが使いやすいなーと思いました。
こめんと欄
|