* buntan-pc #2 -(by [[K]], 2025.04.11) ** (0) -https://github.com/buntan-pc/ -これにかかわる開発の話 ** 2025.04.11 Fri #0 -今のbuntan-pcにはasという専用のアセンブラがあるのだけど、これの互換品をささっと作ってしまいたい。これが当面の目標。 -今のbuntan-pcにはuasという専用のアセンブラがあるのだけど、これの互換品をささっと作ってしまいたい。これが当面の目標。 -そのために、どういうソースを渡せばどういうバイナリがでてくるかという、テストセットが欲しい。それがあればはかどる。 -uchanさんに、「・・・ということでテストセットを3組くらいちょうだい!」と頼んだら、「それくらい公開済みのCコンパイラとアセンブラがあれば自分でできるでしょ」といわれて、確かにそれもそうだと思った。ということでやってみる。こちらの環境はWindows+MS-C。 -uas.exeを作っていたらコンパイルエラーが出た。「//」のコメントの末尾が「取得」になっていると、次の行までコメント扱いされて、コンパイルエラーがいっぱい出る。半角ピリオドを追加してVCをなだめる。UTF-8で書かれたテキストなのに、なんでそうなるのか分からないけど、まあ対処法は簡単なので問題はない。 -上記のコメント問題を直したら、7個の警告が出るだけでビルドは問題なくできた。 -なるほどね、MS-Cには「/utf-8」っていうオプションがあるのかー。これを付けたらコメントいじらなくてもおとなしくなった。 -次はucc.exe。 -まずmakeを用意するのが面倒だったので以下のucc.cを作って代用。 #include "main.c" #include "ast.c" #include "insn.c" #include "symbol.c" #include "token.c" #include "type.c" -問題は2つ。 --libgen.hがないので、dirname()がない。 --以下の記述を嫌がる。 struct GenContext gen_ctx = { parse_ctx.scope, 0, 0, 0, {}, {-1, -1}, 0, {}, print_ast, 0 }; -さてどうしたものか。 0, 0, 0, {0}, {-1, -1}, 0, {0}, print_ast, 0 --にしたらとりあえず文句を言わなくなった。 -じゃあ残るはdirname()だけだな。どこかに落ちてないかな。 --なるほど、こういう仕様なのか。 https://surf.st.seikei.ac.jp/~nakano/JMwww/html/LDP_man-pages/man3/dirname.3.html -Cursorさんに「<libgen.h>のdirnameを作ってください。」って頼んでみたら、作ってくれた。 #include <stdio.h> #include <string.h> char* my_dirname(char* path) { static char buffer[256]; char* last_slash; // パスがNULLまたは空の場合、"."を返す if (path == NULL || *path == '\0') { return "."; } // パスをコピー strncpy(buffer, path, sizeof(buffer) - 1); buffer[sizeof(buffer) - 1] = '\0'; // 最後のスラッシュを探す last_slash = strrchr(buffer, '/'); // スラッシュが見つからない場合、"."を返す if (last_slash == NULL) { return "."; } // スラッシュの後ろを終端文字に置き換える if (last_slash != buffer) { *last_slash = '\0'; } else { // ルートディレクトリの場合 *(last_slash + 1) = '\0'; } return buffer; } --とりあえずこれでいけそうだな。 -これでucc.exeはビルドできるようになったけど、できたucc.exeは挙動が怪しい。じゃあ次の作業はデバッグだな。 -いやでも、uas.exeは問題なく動いているっぽいし、ucc/testsの中にarray.sとかもあるから、とりあえずuccはデバッグしなくてもいいのかー。 ** 2025.04.11 Fri #1 -ある程度テストしてみたところ、手元でビルドしたuas.exeは完璧に動いているっぽい。 ** 2025.04.14 Mon #0 -とりあえずK版のアセンブラは半分くらいできた。 -今のところ、289行。 -本家は section .data の次に section .text を書くというルールで、セクションをこまめに切り替えたりはできないけれど、今作っている版では、セクションの宣言順序は自由だし、途中で何度もセクションを切り替えられる。 ** 2025.04.15 Tue #0 -array.sくらいならアセンブルできるようになった!(かも)。 -まだ出力部を書いてないので、メモリの中でアセンブルできているだけ。バグっているかどうかは分からない。 -数値計算を本家よりは頑張ったので、 push lb3-lb2+1 くらいならできるはず(バグってなければ)。つまりカッコなしの加減算ができる。 -今は356行。 ** 2025.04.15 Tue #1 -db命令にも対応して、サンプルの.sファイルは全部アセンブルできるようになった。 -あとは出力部をかかないとな・・・。 ** 2025.04.15 Tue #2 -出力部もできた。ちょっとバクもあったけど、それも直して、ついに本家のアセンブラと同じバイナリを出せるようになった! -今は408行。まあでも行数はそんなに重要じゃない。1行に詰め込めばどうにでもなるのだから。 -一番苦戦したデバッグは、fopen(exe, "wb")が失敗してNULLを返すというもの。・・・いやいや、なんで失敗するのさ!readじゃないよwriteだよ、普通失敗しないじゃないか! --BZエディタで開いているファイルをfopenで書き込みオープンすると、fp==NULLにされる。なるほど、アクセス権が取れなかったんだね。 --これに気づくまでに30分くらい悩んだ。まさかここで失敗しているなんて思わなかったので・・・。 -あとは些細な問題として、途中から本家が出力している実行ファイルと一致しないで、1バイトずれるという問題があった。でもそれは数秒でわかった。本家は"w"でオープンしていたから、テキストモードになっていたわけだ。それで0x0aを出力すると勝手に0x0dが追加されてしまう。ということで、本家も"wb"でオープンするようにしたら、完璧に出力結果が一致するようになった。 -やったね!(明日upload予定) ** 2025.04.16 Wed #0 -今回アセンブラを作るために使ったaclminiというライブラリについて、自分用のメモ。 Ai intptr_t のtypedef (頻出なので短く書けるようにした、以下同じ動機) Ap void * のtypedef Av void のtypedef Auc unsigned char のtypedef Asc signed char のtypedef AStr char * のtypedef As static の#define AClass(Abc) { ... }; → structしてtypedefしてくれるマクロ aSz(Abc) (intptr_t) sizeof(Abc) の代わり (sizeofはunsignedなので使いにくいから) → (追記)のちに ASz に改名した AMlc : AMalloc - 汎用メモリアロケータ・インターフェース AMlcStd : AMlc Standard - malloc/free/reallocを使って作ったAMlc AMlcStdNumSz : AMlcStd Number/Size - mallocしたブロック数と総mallocバイト数をいつでも確認できるようにしたAMlcStd (メモリリークがないことを確認しやすい) ATokenMgr : ATokenManager - 文字列に対して出現順に0,1,2,...と番号を付けるためのクラス (内部で二分探索するので検索は速い) AExpMem : AExpandableMemory - 可変長配列 -aSzはASzにした方がいいかもしれない。そうすればAで始まる名前がこのライブラリ所属だとわかる。aで始まる名前は解放される。 --(追記)ということで aSz は廃止して ASz にした。 ** 2025.04.16 Wed #1 -https://essen.osask.jp/files/kuas00a.zip (4.79KB) --ソースファイルのみです。書いてないけどライセンスはKL-01です。buntan-pcにマージされたものは、MITライセンスになります。 --コンパイルの際は、 kuas.c のみコンパイルすればいいです。aclmini.cは勝手にインクルードされます。 --うわー、ソースコードだけでzip化すると5KB未満かー。小さいなー。 -今回かなり短期間で書けたわけですが、それで思ったのは、もう整合性の取れた仕様が決まっていて、それに合わせて作るだけでよければ、それほど大変じゃないってことなんだろうと思います。それに本家のuasはシンプルに作れるように仕様が工夫されているのもあると思います。 ** 2025.04.16 Wed #2 -「--pmem」「--dmem」オプションに対応してほしいと言われたので対応しました。 -https://essen.osask.jp/files/kuas01a.zip (4.92KB) --ソースファイルのみです。書いてないけどライセンスはKL-01です。buntan-pcにマージされたものは、MITライセンスになります。 --コンパイルの際は、 kuas.c のみコンパイルすればいいです。aclmini.cは勝手にインクルードされます。 ** 2025.04.18 Fri #0 -const-expr.sにも対応してほしいと言われたので対応しました。行数的には10行の増加になりました。 -https://essen.osask.jp/files/kuas02a.zip (5.13KB) --ソースファイルのみです。書いてないけどライセンスはKL-01です。buntan-pcにマージされたものは、MITライセンスになります。 --コンパイルの際は、 kuas.c のみコンパイルすればいいです。aclmini.cは勝手にインクルードされます。 ** 2025.04.19 Sat #0 -今日はkuasには直接関係しない話を書きます。 -インタプリタやコンパイラを作っていると、式の評価の処理が必要になります。例としては、[[a21_txt01_7]]の、expr()+exprSub()+exprSub1()みたいな処理です。 -私が上記の処理を書いたのは2021年で、その後、数回の改良を加えて、もっときれいにすっきりと書けるようになりました(私だって少しは成長するのです)。 -とはいえ、言語を作るたびに似たような処理を書くというのはいかにも非効率だと思いました。インタプリタでもコンパラでもどちらの用途でも使えて、整数でも浮動小数点でもString型でもユーザ定義のオブジェクトでも何でもいけるような、そういう汎用的なプログラムを一度作るべきなのです。そうすれば、その後は似たような処理を書かずに済みます。 -まずどのくらいの汎用性を持たせるかを考えました。 --パースのアルゴリズムは規定しないで、言語側に裁量を持たせる。 --演算子の優先順位について。C言語に規定されている演算子については、基本的にはC言語と同じだとする。C言語とあまりに違うルールを採用すると、もはや混乱して使いこなせない気がするので。 --とはいえ、bitAndやbitOrなどのbit演算は比較演算子よりも優先されてほしいという気はする。しかしそれでもここを変更するとややこしいので、優先順位の異なる新たな演算子を追加してbitAndやbitOrを実現するのが一番スマートかなとも思う。・・・ということで基本的に既存の演算子はそのまま使うイメージになる。 --だから演算子の追加が自由かつ低コストでできればよい。 --どんな型をどうサポートするのかは言語側の裁量なので、このライブラリでは何も決めずに丸投げする。 -そういうことを考えて、適当にいい感じに作ってみました。 -結局こんな感じになりました。 AClass(ExprDriver) { // これはExprDriverの内部状態を保持するためのクラス. const char *s, *s0; // もっともシンプルに行くなら、これだけでやれる. もちろんもっと複雑でもよい(言語側で好きなように決める). }; AClass(ExprObject) { intptr_t typ; ... }; // 言語側で好きなように決める. ExprObject *ExprDriver_func(ExprDriver *driver, intptr_t func, ExprObject *obj0, ExprObject *obj1) { ... } // 話を分かりやすくするためにいったん省略. int main(int argc, const char **argv) { if (argc >= 2) { ExprDriver driver; driver.s = argv[1]; ExprObject *result = (ExprObject *) AExpr(ExprDriver_func, &driver, 999); // ここでresultを表示. } return 0; } -これだけで行けます。ExprDriver_func()が言語処理系依存の処理を全部やります。AExpr()から見るとコールバック関数です。 -AExpr()の最後の999は演算子の優先順位です。優先度999までの演算をすべて完了してから結果をかえすように指示しています。これは 1+2*3+4 みたいな式があって、「1+2*」まで処理したときに、次に3を取ってくる処理があるわけですが、この場合、3+4ではなく、3で打ち切って取ってこないと次の乗算の結果がおかしくなるわけです。このように式の途中までで評価を打ち切る必要が内部的にはあるので、演算子の優先順位を最後に書くことになっています(AExprはAExprを再帰的に呼び出して式の評価を実現しています)。・・・いきなり何を言っているのか分からなかったかと思いますが、基本的に999を書いておけば全部正しく最後まで処理されるので、気にしないで999を指定しておけば問題ありません。 -こんな ExprDriver_func() に丸投げばかりの仕様なので、 ExprDriver_func() が複雑になって、結局誰も得しないってことになりそうなわけですが、しかしドライバ側で優先順位がどうなっているかはまったく気にしなくてよくなります。AExprが矢継ぎ早に正しい順序で呼び出してくるので、受け身に徹してこなしていれば、自然にすべて無駄なく処理されます。これが気持ちいいのです!(笑)。余計なことで頭を使わずに済みます。 func==0: 対象となる数式から次の1トークンを取ってくるだけの処理です。ExprObjectで返します。 演算子なのか何らかの値を持つオブジェクトなのかは、ExprObject内の先頭にあるtypメンバの値で認識されます。 func==1: 指定されたExprObjectをメモリ開放します。どんな開放処理が必要なのかAExprにはわからないので、ドライバ関数に依頼する形になっています。 まあたいていはfreeするだけの簡単な処理になります。 func==2: 直前にfunc==0で読み取ったトークンを、押し戻します。ungetc()みたいなやつです。 こういうことをできるようにするもっとも簡単な方法は、func==0のときに、元のsの値をs0に保存しておくことです。 このやり方なら、func==2が来た時に、s = s0; を実行するだけでおしまいです。 func==3: リザーブです。今のところこの呼び出しはありません。 func==4: 単項演算子を新規に追加したい場合、もしくは既存の単項演算子の挙動をオーバーライドしたい場合に、この関数で処理します。 追加も変更もないなら、ただ0を返しておけばいいです。 func==5: 二項演算子、三項演算子などを新規に追加したい場合、もしくは既存の演算子の挙動をオーバーライドしたい場合に、この関数で処理します。 追加も変更もないなら、ただ0を返しておけばいいです。 func==AExpr_Add: 二項演算子「+」による加算です。2つのExprObejectを引数として受け取り、演算して、結果が入ったExprObjectを返します。 他にも演算子の数だけ同様の呼び出しがあります。対応したくない演算子に関しては、演算結果ではなくエラーオブジェクトを返せば、 問題なくエラー処理されます。・・・だから処理したい演算子だけ書いておいて、最後に「それ以外は全部エラーを返す」と書くわけです。 ** 2025.04.20 Sun #0 -uchanさんのComProcのCPUのアセンブラで書かれたプログラムの多くを、一般的なC言語プログラムに移植するアルゴリズムを思いついた「かもしれない」。 -同じアルゴリズムはたぶんx86とかx64とかARMとかAArch64とかRISC-Vのアセンブラにも適用できそう。 --エミュレータのいらない世界が実現したらいいなあ。 -これが実現したら、CPUを自作したときに、たくさんのプログラムを簡単に移植してくることができる・・・とかできたらいいなあ。 -(追記)もう少しだけ詳しい話を書いてみる。 -たいていのアセンブラでは、CALL命令(もしくはそれに相当する命令)がある。これはC言語で言うと関数呼び出しに相当する。 -これを一般的なC言語プログラムでどう表現するかという問題があった。 -まず前提として、アセンブラのラベルにはCALL用かJMP用かという区別はない。通常はCALLで呼ぶようなラベルを最適化としてJMPで分岐する場合すらある。 --これはCALL命令の直後にRET命令があるときとかに、よく用いられる最適化。 -こういうことをされると、「CALLされることがあるラベルかどうかを集計して、CALLされるものは関数としてC言語化する」という戦略が使えない。なぜならC言語では、関数に対してgotoはできないから。 --これに対しては、関数に対するJMPをCALL+RETに変換すればいいかなと考えたこともあったけど、スタックの深さとかが狂ってくるので、たぶんうまくいかない。 -そもそもC言語においてはgoto用のラベルと関数はかなり異なる。関数はスタックフレームの構築処理などもあり、そんなに簡単に代用できる話ではない。 -そうなると、CALL命令はリターン用のラベルを宣言してその値をスタックに積み(もしくはしかるべきレジスタに代入し)、その上でJMPする、という処理に変換するのがよさそうに思える。 --[註]RISC系の一部のCPUではCALL/RETでスタックを使わず、専用のレジスタにリターン先を代入する。関数側でこのレジスタを退避・復元することで、関数呼び出しネストを実現する。 -でもこれは難しい。goto用のラベル値を取得する方法がない。gccの独自拡張にはあるが、それだとgccでしかコンパイルできないことになる。 --http://cms.phys.s.u-tokyo.ac.jp/~naoki/CIPINTRO/gccextend.html --というかgccを作った人は、本当によく考えている。この機能はプログラミングの可能性を大きく広げてくれる。 -CALL命令はほぼすべてのアセンブラプログラムに出てくるくらいのメジャーな命令なので、これをコンバートできないなら「一般的なC言語プログラムに移植する」という基本アイデアは捨てなきゃいけない。 -ここからが今回の思い付き。プログラム全体を巨大なswitch-caseでラッピングする。プログラムの一番最初にswitchを置く。そして「goto用のラベル値」ではなく、「switchで分岐するための値」を取得するという方式にする。この値を適当な変数に代入して、プログラムの先頭にgotoすれば、swicth-caseによって狙った場所に分岐できる。 --うーん、前提の部分も含めて、この説明じゃ何を言いたいのかよく分からないだろうなあ。実際に作って見せるのが一番早そう。 ** 2025.04.20 Sun #1 -一方で、安野さんたちのvibe codingの話とかを見ていると、私なんかがあれこれ考えなくても、全部AIが何とかしてくれる未来の方が先に来るんじゃないかなっていう気分になる・・・。 --https://x.com/takahiroanno/status/1913121506273710241 ** 2025.04.21 Mon #0 -たまに、どうやってプログラミングの勉強をしたらいいですか?と聞かれます。私の考えを書きます。 -まずとにかくプログラムを書きます。OSに関心がある時はOSを何個も書きます。データ圧縮に関心がある時は圧縮プログラムを何個も書きます。プログラミング言語に関心がある時は言語を何個も作ります。そうやって色々作っていると、自分の作るプログラムには共通部分があることが分かります。それはライブラリ化して、何度も何度も書かないでいいようにするべきです。・・・こういうことを繰り返していると、自分の開発力が底上げされてきます。 -私は自分が作ったプログラムのすべてを常用しているわけではありません。それでもいいのです。でも反省はします。なぜ使わないのかと。プログラムを書いても使わないなら、そんなの作らない方がいいじゃないですか。ライブラリだって使わないなら作らなくていいじゃないですか。・・・この反省を生かして次の開発をすると、どんどんうまくなっていくのです。 // subはp0,p1だけではなく、opの情報も必要. 具体的には (type) とか。 // cast演算子(typオブジェクト, valオブジェクト)にできればとても良いが、できるだろうか? // (typ)を検出したら、まずtypをオブジェクトとして送出する。次にcast演算子を送出する。残りは普通に結合する。これで行けるのでは? // ?の3項演算子はどうするのか。まず?が来る。スタックする。:が来る。スタックトップを見て処理する。 // 単項演算子と二項演算子の違いは、実際の項の数ではなく、演算子の前にオブジェクトが必要か必要ではないかの差でしかない。