概要
- Python で500行以内の Cコンパイラ を作成した挑戦記
- 単一パス で構文解析とコード生成を同時に実施
- WebAssembly をターゲットとした独自設計
- いくつかのC言語機能を 省略 しつつ、主要機能を実装
- 補助クラス や構造についても解説
500行Python製Cコンパイラ開発記
目標と概要
- Python で 500行 以内にCコンパイラを実装する試み
- 省略可能な機能を削り、 シンプル かつ 理解しやすい 設計を目指す
- ブログ記事内で全コードは紹介できないため、 設計判断 や 削除した機能、 アーキテクチャ の概要を解説
単一パスコンパイラの選択
- 単一パス 方式を採用し、構文解析中に即時コード生成
- 構文木(AST) を生成しない設計
- 一般的なコンパイラは「構文木生成→コード生成」の 2パス 構成
- 解析とコード生成を分離しやすい
- 最適化 や 中間表現 の導入が容易
- 500行制限のため、 解析と生成を統合
- 例: ~演算子 のパース時に即時WebAssembly命令を出力
- 構文木ノード や中間表現は未使用
WebAssemblyをターゲットに選定
- WebAssembly を出力対象とした理由は興味本位
- WebAssembly はC言語向きではなく、独自の難しさ
- goto非対応、代わりに block/break構造 を利用
- レジスタ非搭載、 スタックマシン 方式
- Cの スタック とWASMのスタックは別管理が必要
- 自前で メモリ上のスタック を用意
- x86/ARM等の従来ISAより 複雑化 した可能性
エラーハンドリング
- die関数 による簡易的なエラー処理のみ
- 異常時は スタックトレース と曖昧なエラーメッセージを出力
- Rustコンパイラ のような高機能エラー表示は非対応
実装しなかった機能
- struct、 enum、 union、 プリプロセッサ、 浮動小数点型、 long/double
- 多次元配列、 前置/後置インクリメント、 キャスト式、 標準ライブラリやI/O
- typedef や ポインタ 等、C言語実装の肝となる部分は維持
実装済み機能
- 算術演算子 と 優先順位 の正しい処理
- int/short/char型、 文字列定数 (エスケープ対応)
- 多重ポインタ、 正しいポインタ演算
- 一次元配列、 関数定義・呼び出し
- typedef 対応(lexer hack活用)
テストケースとサンプルプログラム
- c-testsuite の34/220テストをパス
- swap関数 や フィボナッチ関数 等、実用的なCプログラムも実行可能
補助クラス群の概要
-
Emitter
- WebAssemblyコードの 整形出力 を担当
- インデントや no_emit メソッドによる出力制御
-
StringPool
- 文字列定数を 連続領域 にまとめて管理
- 既存文字列の再利用や アドレス管理 を実装
- 最終的に rodataセクション として出力
-
Lexer
- C言語の トークン分割 を担当
- peek/next/try_next による柔軟なトークン取得
- lexer hack でtypedefや型名判定を実現
- 型情報を動的に Lexer へフィードバックし、型名/変数名の区別を管理
-
CType
- C言語型( int や short など)の情報を管理するデータクラス
- ポインタレベル や 配列サイズ の情報も保持
- 多次元配列 は非対応
-
FrameVar/StackFrame
- C言語の スタックフレーム管理 を担当
- WASMのスタックが使えないため、 独自のメモリスタック を構築
- 各関数ごとに ローカル変数・引数 のスロット割当と スタックサイズ計算
まとめ
- 500行制限 の中で、C言語の本質的な機能を Python で実装
- 単一パス や WebAssembly対応 など、制約下での工夫が満載
- 構文木や最適化を省略しつつ、 実用的なCサブセット をコンパイル可能
- 補助クラス もシンプルながら必要十分な設計