このページでは、ここで作るBの処理系Mae-Bについて概要を説明します。
ソースはGitHubに上げてあります。
https://github.com/kmaebashi/blang
Mae-BはWindows11のWSL2で動くUbuntu Linuxで、Cで作りました。Cの前身であるはずのB言語の処理系をCで作る、という時点で何かが倒錯している気がしますが。
CコンパイラはGCCで、-ansi -pedanticオプションを付けることでANSI C/C89モードにしています。いまどきC99ぐらい使えばよかったのでは? とさすがに思わなくもないです。
実際のコーディングは、実に久しぶりにEmacsで行いました。ウインドウをふたつ開いて左に.cファイル、右にヘッダファイルを開いて作業すれば補完なんかなくてもなんとかなるものです。デバッガはGDB。
AIによるコード生成は使ってません。コーディングなんて一番楽しい時間をわざわざAIにくれてやる必要はないじゃないですか!!
質問をAIに投げるのはちょくちょくやったけど、Bの話になると本当にでたらめしか言わねえ……
Mae-Bは、コンパイラがオンメモリで生成したバイトコードをBVM(B Virtual Machine)がインタプリタで実行する、という形態になっています。
BVMはJavaのJVMと似たようなよくあるスタックマシンです。
たとえば、「1から、引数で与えられた数までを加算する」という関数を含むBプログラムsum_test.bは以下のようになります。
main() {
auto sum;
sum = sum_func(100);
printf("sum..%d*n", sum);
}
sum_func(num) {
auto sum, i;
sum = 0;
i = 1;
while (i <= num) {
sum =+ i;
i++;
}
return sum;
}
ここでは、関数sum_func()に引数として100を渡しているので、1から100までを加算した値、5050が表示されます。
このソースコードを、今回作成したMae-B BVMのバイトコードにコンパイルすると、以下のようになります。右に赤字でコメントを付けておいたので、BVMのインストラクションリファレンスも参照すればだいたい何をするのかわかるのではないでしょうか。
13 PUSH_N 1 # スタックポインタを1進めてローカル変数sumの領域を確保 15 PUSH 100 # sum_funcの引数である100をスタックにpush 17 PUSH 1 # 引数の数である1をスタックにpush 19 PUSH 44 # sum_funcの開始アドレスである44をスタックにpush 21 CALL # スタックトップのアドレスの関数(sum_func)を呼び出す 22 POP_N 2 # 戻ってきたので引数分のスタックを開放する 24 PUSH_RETURN_VALUE # (レジスタで受け取った)戻り値をスタックに積む 25 POP_AUTO 0 # ローカル変数sumにスタックトップの値(戻り値)をPOPする 27 PUSH_AUTO 0 # ローカル変数sumの値をスタックにpush 29 PUSH 89 # 文字列リテラル"sum..%d*n"のアドレスをpush 31 PUSH 2 # 引数の数である2をスタックにpush 33 PUSH 0 # printfの(疑似)開始アドレスをpush 35 CALL # スタックトップのアドレスの関数(printf)を呼び出す 36 POP_N 3 # 戻ってきたので引数分のスタックを開放する 38 PUSH 0 # 戻り値として、0をスタックに積む 40 SAVE_RETURN_VALUE # スタックトップの値をレジスタに退避する 41 POP_N 1 # ローカル変数sumの領域を解放 43 RETURN # リターンする(mainが終わるので、処理終了) 44 PUSH_N 2 # ここからはsum_func。ローカル変数領域確保 46 PUSH 0 # 「sum = 0」の0をpush 48 POP_AUTO 0 # 上でpushした0をsumにpop 50 PUSH 1 # 「i = 1」の1をpush 52 POP_AUTO 1 # 上でpushした1をiにpop 54 PUSH_AUTO 1 # iの値をスタックにpush 56 PUSH_AUTO 5 # 引数numの値をスタックにpush 58 LE # 「i <= num」の比較 59 JUMP_IF_FALSE 77 # 結果がFALSEなら77にジャンプ 61 PUSH_AUTO 0 # sumの値をpush 63 PUSH_AUTO 1 # iの値をpush 65 ADD # sumとiを加算した値をスタックにpush 66 POP_AUTO 0 # スタックトップの値をsumにpop 68 PUSH_AUTO 1 # iの値をpush 70 PUSH 1 # i++で1加算するための1をpush 72 ADD # iと1を足した値をpush 73 POP_AUTO 1 # スタックトップの値をiにpop 75 JUMP 54 # ループの先頭にジャンプ 77 PUSH_AUTO 0 # sumの値をpush 79 SAVE_RETURN_VALUE # スタックトップの値をレジスタに退避する 80 POP_N 2 # ローカル変数sumとiの領域を解放 82 RETURN # リターンする 83 PUSH 0 # returnが書かれなかった場合に備え、return 0を生成 85 SAVE_RETURN_VALUE # (このケースでは単なる無駄になっている) 86 POP_N 2 88 RETURN
これを見るとわかるように、BVMのバイトコードは、何らかの命令に、最大ひとつのオペランド(引数)がついたものになっています。
――オリジナルのBは、Dennis RitchieのThe Development of the C Languageによれば、
The B compiler on the PDP-7 did not generate machine instructions, but instead `threaded code' [Bell 72], an interpretive scheme in which the compiler's output consists of a sequence of addresses of code fragments that perform the elementary operations. The operations typically—in particular for B—act on a simple stack machine.
訳)PDP-7上のBコンパイラは機械語命令を生成せず、代わりに「スレッディッドコード」[Bell 72]を生成した。これは解釈型の手法で、コンパイラ出力は基本操作を実行するコード断片のアドレス列で構成される。操作は典型的に——特にBの場合——単純なスタックマシン上で動作する。
とあります。「スレッディッドコード」というのはWikipediaにもありますが※1、上に「基本操作を実行するコード断片のアドレス列で構成される」とあるので、アドレス列がずらっと並んでいて、それが「基本操作を実行するコード断片」のそれぞれの開始アドレスを指していて、インタプリタはそのアドレス列に順次ジャンプしていく、という作りのようで、Javaのような「バイトコードインタプリタ」とはちょっと違うものだったようです。
ディレクトリ構成はこんな感じです。
blang ├ Makefile ← 全体のMakefile ├ main.c ← main()関数を含む ├ 📁compiler ← コンパイラ部分 ├ 📁bvm ← BVM(B Virutal Machine)部分 ├ 📁compiler ← コンパイラ部分 ├ 📁memory ← メモリ管理ルーチン ├ 📁include ← 共有するヘッダファイルの置き場所 ├ 📁btut ← Bチュートリアルのサンプルプログラム群 └ 📁bref ← Bリファレンスマニュアルのサンプルプログラム群
トップディレクトリでmakeを動かせば、順次下のMakefileが動いて、トップディレクトリにblangという名前の実行形式ができるはずです。
以下のように「blang <Bソースファイル名>」の形式で実行します。
$ ./blang sum_test.b
ここで使っているメモリ管理ルーチンや、関数の命名規則等については、以前「プログラミング言語を作る」で書いた時と同じなので※2詳細はこちらのページを参照してください。
公開日: 2026/01/18
間違い等ありましたら、掲示板にご連絡願います。