十数年前、自分の中でFPGAがブームになり、MIPS互換のCPUなどを作ったりしていました。
独自仕様のCPUも作ってみたかったのですが、コンパイラを作ることも既存のものをポーティングすることもできなかったので諦めました。
それ以来、いつかはコンパイラを書いてみたいと思っていました。

年齢を重ねて、プログラムを書けなくなる(興味を失うことも含む)前に、書いてみることにしました。
書き始めたのは3年以上前で、ある程度作っては飽きて放置、しばらくして再開、を繰り返し、やっと形になったので公開します。
ターゲットプロセッサはコンパイラを作るのが楽になるように決めた独自仕様です。

レジスタ(整数/浮動小数点数兼用)
汎用レジスタ: 64ビットx16本
R0:	ゼロレジスタ
R1-8:	演算ワーク
R9:	返値
R10-13:	引数
R14:	グローバルポインタ
R15:	スタックポインタ
PC:	プログラムカウンタ
フラグ:	割込み許可ビットのみ
インストラクションセット
ST/LD/LDUのszは1,2,4,8のいずれか
LI	Rd,imm		Rdのbit15-0にimmを符号拡張してロード
LUI	Rd,imm		Rdのbit31-16にimmをロード
LWI	Rd,imm		Rdのbit63-16にimmをロード(ISA64のみ)
LEA	Rd,ofs(Rs)	ofs(Rs)の実効アドレスをRdにロード
ST.sz	Rs,ofs(Rd)	Rsの下位szバイトをofs(Rd)にストア
LD.sz	Rd,ofs(Rs)	ofs(Rs)からszバイトを符号拡張してRdの下位にロード
LDU.sz	Rd,ofs(Rs)	ofs(Rs)からszバイトを0拡張してRdの下位にロード
MOV	Rd,Rs		Rd=Rs
ADDI	Rd,imm		Rd+=imm(符号拡張)
ADD	Rd,Rs		Rd+=Rs
SUB	Rd,Rs		Rd-=Rs
MUL	Rd,Rs		Rd*=Rs
DIV	Rd,Rs		Rd/=Rs
REM	Rd,Rs		Rd%=Rs
MULU	Rd,Rs		Rd*=Rs(符号無し)
DIVU	Rd,Rs		Rd/=Rs(符号無し)
REMU	Rd,Rs		Rd%=Rs(符号無し)
ADDF	Rd,Rs		Rd+=Rs(浮動小数点数)
SUBF	Rd,Rs		Rd-=Rs(浮動小数点数)
MULF	Rd,Rs		Rd*=Rs(浮動小数点数)
DIVF	Rd,Rs		Rd/=Rs(浮動小数点数)
NEGF	Rd		Rd=-Rd(浮動小数点数)
CVTF	Rd		Rdを整数とみなし、浮動小数点数に変換
CVTI	Rd		Rdを浮動小数点数とみなし、整数に変換
COM	Rd		1の補数
NEG	Rd		2の補数
AND	Rd,Rs		Rd&=Rs
OR	Rd,Rs		Rd|=Rs
XOR	Rd,Rs		Rd^=Rs
ANDI	Rd,imm		Rd&=imm
ORI	Rd,imm		Rd|=imm
XORI	Rd,imm		Rd^=imm
SLL	Rd,imm		Rdをimmビット左シフト
SRL	Rd,imm		Rdをimmビット算術右シフト
SRA	Rd,imm		Rdをimmビット論理右シフト
SLLV	Rd,Rn		RdをRnビット左シフト
SRLV	Rd,Rn		RdをRnビット算術右シフト
SRAV	Rd,Rn		RdをRnビット論理右シフト
SEQ	Rd,Rs		Rd==RsをRdにセット
SNE	Rd,Rs		Rd!=RsをRdにセット
SLT	Rd,Rs		Rd<RsをRdにセット
SLE	Rd,Rs		Rd<=RsをRdにセット
SGT	Rd,Rs		Rd>RsをRdにセット
SGE	Rd,Rs		Rd>=RsをRdにセット
SLTU	Rd,Rs		Rd<RsをRdにセット(符号無し)
SLEU	Rd,Rs		Rd<=RsをRdにセット(符号無し)
SGTU	Rd,Rs		Rd>RsをRdにセット(符号無し)
SGEU	Rd,Rs		Rd>=RsをRdにセット(符号無し)
SEQF	Rd,Rs		Rd==RsをRdにセット(浮動小数点数)
SNEF	Rd,Rs		Rd!=RsをRdにセット(浮動小数点数)
SLTF	Rd,Rs		Rd<RsをRdにセット(浮動小数点数)
SLEF	Rd,Rs		Rd<=RsをRdにセット(浮動小数点数)
SGTF	Rd,Rs		Rd>RsをRdにセット(浮動小数点数)
SGEF	Rd,Rs		Rd>=RsをRdにセット(浮動小数点数)
BRA	label		分岐
BEQ	Rs,Rt,label	Rs==Rtなら分岐
BNE	Rs,Rt,label	Rs!=Rtなら分岐
BEQF	Rs,Rt,label	Rs==Rtなら分岐(浮動小数点数)
BNEF	Rs,Rt,label	Rs!=Rtなら分岐(浮動小数点数)
CALL	label		コール
CALLV	Rs		Rsをアドレスとみなしてコール
RET			リターン
RETI			割込みからのリターン
EI			割込み許可
DI			割込み禁止
SLEEP			割込みがあるまで停止
NOP			ノーオペレーション
このプロセッサを使ったPCの仕様は以下のようにしました。

メモリ(2Mバイト)
0x000000		リセット時に実行する命令
0x000004-0x00001f	割込み(1-7)時に実行する命令
0x000020-0x03ffff	テキストエリア
0x040000-0x13ffff	データおよび変数エリア
0x140000-0x1dffef	VRAM(640x480,16bit color)
0x1dfff0-0x1dffff	I/O
0x1e0000-0x1fffff	スタックエリア
入力
ofs
0	キーデータ
1	キーモディファイア
4	ファイル入力
5	EOF
出力
ofs
0	コンソール
2	PSGアドレス
3	PSGデータ
4	LBA bit7-0
5	LBA bit15-8
6	LBA bit23-16
PSG
PSGアドレスbit7-3で32個のオペレータの中から1つを選び、bit2-0で以下の機能を選ぶ
その後PSGデータにデータを書き込む
0	加算値下位
1	加算値上位(0xffだとノイズ)
2	ボリューム左
3	ボリューム右
4	波形選択(矩形波デューティ50%/矩形波デューティ12.5%/鋸波/三角波)

開発方針

Cで書かれたソースの多くをコンパイルできることを目標にしますが、以下の制限を設けます。

ビットフィールド無し
キーワードvolatile,register,autoは無視
演算用レジスタは8本で、演算中に保持する値がこれを超える複雑な式はコンパイルエラーとする

また、intとlongは32ビット、long long,float,doubleは64ビットです。

Cコンパイラを書くときは(セルフホスティングが目的などの理由で)Cで書くことが多いようですが、今回は楽ができるC++17で書きました。
また、短時間しか走らないプログラムなので、一度確保したメモリは終了まで解放しない方針で書きました。
これも、解放を考えなければとても楽だからです。

使用したライブラリ
コンパイラがあっても、標準ライブラリ的なものがないと実用になりません。
既存のものはどうしてもOSに依存するのですが、多少編集すれば依存を無くせるものを見つけました。

https://git.kernel.org/pub/scm/libs/klibc/klibc.git

それでもstdio周りは辛いので、ChaNさんのものを使わせていただきました。

Embedded String Functions/FatFs Module/TJpgDec Module
http://elm-chan.org/fsw.html

JPEGデコーダはサンプルのビューアに使用しています。

フォントは下記のものを使わせていただきました。

M+ BITMAP FONTS
http://mplus-fonts.sourceforge.jp/mplus-bitmap-fonts/index.html

オブジェクトはアセンブルリスト
実験的なコンパイラなので、オブジェクトファイルのフォーマットはアセンブルリストの形式になっています。

tjpgdライブラリをコンパイルした例です。
#define BYTECLIP(v) Clip8[(unsigned int)(v) & 0x3FF]
static const uint8_t Clip8[1024] = {
...
};
...
const int CVACC = (sizeof (int) > 2) ? 1024 : 128;
...
int yy, cb, cr;
のように定義されていて、次の式:
*pix++ = BYTECLIP(yy + ((int)(1.402 * CVACC) * cr) / CVACC);
は以下のようにコンパイルされるのが簡単に確認できます。
;#L838  *pix++ =   Clip8[(unsigned int)(yy + ((int)(1.402 * CVACC) * cr) / CVACC) & 0x3FF];
T 00001F64 d 2F1E80C0   lea     r1,-32576(r14)  ;Clip8
T 00001F68 - 3A2F0050   ld.4    r2,80(r15)      ;yy
T 00001F6C - 3A3F0048   ld.4    r3,72(r15)      ;cr
T 00001F70 - 1B40059B   li      r4,1435
T 00001F74 - 42340000   mul     r3,r4
T 00001F78 - 1F30000A   sra     r3,10
T 00001F7C - 40230000   add     r2,r3
T 00001F80 - 182003FF   andi    r2,1023
T 00001F84 - 40120000   add     r1,r2
T 00001F88 - 3C110000   ldu.1   r1,0(r1)
T 00001F8C - 3B2F0030   ld.8    r2,48(r15)      ;pix
T 00001F90 - 14200001   addi    r2,1
T 00001F94 - 332F0030   st.8    r2,48(r15)      ;pix
T 00001F98 - 3012FFFF   st.1    r1,-1(r2)
各行は、セクション、オフセット、リンカへの指定、バイナリ、ニーモニックとなっています。
上の例のように、一つの式の範囲内では割とまともに最適化します。

ライブラリの代わりにリンクポインタを実装
通常の開発ツールではコンパイルしたオブジェクトファイルをarなどでまとめてライブラリを作り、利用する側は-lオプションをつけてリンク指定します。
このコンパイラでは複数のソースをコンパイルするときに-aオプションをつけるとリンクパスファイルを生成します。
これは、以下のようにシンボルとそれが定義されているファイルパスを並べただけのファイルです。
jd_decomp tjpgd3/src/tjpgd.o
jd_prepare tjpgd3/src/tjpgd.o
これを利用する側は-lオプションで指定するとあたかもライブラリのようにリンクできる仕組みです。

割込みハンドラ
特殊な機能として、関数名がinterrupt[1-7]だと割込みハンドラに設定されます。

サンプルの使い方

テキスト/グラフィックの描画デモ、マンデルブロ集合、MIDIプレイヤー、JPEGビューアのサンプルです。

ホームディレクトリにfat.dmgという名前のディスクイメージを用意します。
ディスクイメージはFATでフォーマットし、ルートディレクトリにMIDIとJPEGという名前のディレクトリを作ります。
MIDIディレクトリには拡張子が.midのSMF0フォーマットのファイルを、JPEGディレクトリには拡張子が.jpgのファイルを入れ、アンマウントしておきます。

cckp.xcodeprojを開いてリリース版をビルドします。デフォルトのビルドディレクトリにコンパイラが生成されます。
次にトップディレクトリでmakeするとa.srecができるので、vmkp.xcodeprojを開いて実行し、vmkpでa.srecを開いて実行します。

vmkpのオマケ機能として、関数単位のタイムプロファイリングができます。
以下はJPEGビューアを実行した例です。
28.3% mcu_output (tjpgd.o)
18.1% block_idct (tjpgd.o)
16.2% memset (memset.o)
13.1% huffext (tjpgd.o)
11.4% mcu_load (tjpgd.o)
 6.3% bitext (tjpgd.o)
 4.0% outfunc (viewer.o)
 1.6% disk_read (diskio.o)
 0.4% jd_decomp (tjpgd.o)
 0.3% infunc (viewer.o)
 0.1% f_read (ff.o)
 0.1% memcpy (memcpy.o)

https://github.com/kwhr0/cckp

macOS11.0以上