Pythonのコンパイラを作りたい #1 - 開発の背景と概要
こんにちは。これから続ける(かもしれない)「Pythonのコンパイラを作りたい」では私が取り組んでいる「PythonコードをLLVM IRへ変換し、ネイティブバイナリを生成する」プロジェクト pyc
のあれこれをご紹介します。
初回のこの記事では、開発の背景・モチベーションや全体像など、「なぜわざわざPythonのコンパイラを作るのか?」という部分をざっくりとお話ししようと思います。
1. Pythonコンパイラを作る理由
1-1. なぜPythonをコンパイルする必要があるのか?
Pythonはとても書きやすく、多くのライブラリが揃っており、数値計算・機械学習・Web開発・スクリプト処理など、幅広い分野で利用されています。しかし、インタプリタ言語としての宿命もあり、やや速度面で不利な場合があります。特に、大規模データの処理やリアルタイム性が重要な場面では、パフォーマンスがボトルネックになることも珍しくありません。
そこで「もっと気軽に Python のコードを速く動かしたい!」と思い立つ方は多いでしょう。実際に高速化のために以下のような手段がありますが、どれも一長一短です。
- C拡張: 速度は出やすいが、C言語の知識が必要。
- Numba / PyPy: 特定の計算に対しては有効だが、適用できるコードや文法に制限があったり、ランタイムの互換性に注意が必要。
これらの方法を使いこなすのも良いのですが、私の場合「それならもうPythonのソースを完全にLLVM IRへ落とし込み、最終的に機械語へコンパイルしてしまえばいいのでは?」という好奇心が強く働きました。これが pyc
を作り始めた最初のモチベーションです。
1-2. PythonをWebAssemblyで動かしたい
もう一つ大きな動機は、Python を WebAssembly にコンパイルしてブラウザ上で動かしてみたい、というアイデアです。JavaScript が動作する環境であれば、理論上、WebAssembly 形式でコンパイルした Python が動くはずです。
すでに Pyodide と呼ばれる Mozilla の取り組みが有名ですが、これは実質的に CPython ランタイムをそのまま WebAssembly に移植しており、かなり大きなバイナリサイズになりがちです。より軽量に、独自ランタイムの要素を最小限に絞り込めば「Pythonがそのままブラウザ上でネイティブ並みに動く」仕組みを実現できるかもしれません。
1-3. ランタイムを自前実装するとどうなるか
PyInstaller や Pyodide のように CPython を丸ごと含めて配布するアプローチは、多機能ですが実行ファイルのサイズがどうしても肥大化しがちです。それに対し pyc
では「Python らしい構文を維持しつつ、最低限のランタイム機能だけを持たせる」という方針を取りました。C言語で書かれたランタイムとリンクするだけで動作する形にすれば、最終的にバイナリも軽くなります。
もちろん、実装の手間はかかりますが、機能を絞ったぶん高速化や改変の自由度を得ることができる、というのが大きな魅力です。
2. 既存プロジェクトとの違い
Cython
2-1.Cython は Python に型注釈を追加した .pyx
ファイルをコンパイルする仕組みで、数値計算や C 拡張モジュールの開発に使われることが多いです。ただし、Cython 特有の文法習得が必要であったり、「100% 純粋 Python コードをそのままコンパイル」というわけにはいきません。
pyc
の目標は、なるべく純粋な Python コード (と型アノテーション) を使いながら、LLVM IR を生成しようという点で異なります。
Numba
2-2.数値計算や科学技術計算に強い Just-in-Time コンパイラです。高速化される部分はとても速いのですが、対応する構文や型が限定的だったり、GPU向け機能など特殊な部分に特化しているところもあります。また、JITコンパイルであって、スタンドアロンのネイティブバイナリを作るわけではありません。
PyPy
2-3.PyPy は高速な JIT を搭載した Python 実装で、CPython より多くの場合で高速に動く可能性があります。ただし、PyPy 自体が独自のランタイムとガーベジコレクタを持ち、C 拡張モジュールとの互換性など、環境によっては難しい面もあります。
pyc
は JIT ではなく AOT (Ahead-Of-Time) コンパイラ風に「Python コードを一度 LLVM IR へ落として最終的にネイティブバイナリを得る」ため、PyPy とはアーキテクチャが大きく異なります。
pyc
の目指す立ち位置
2-4. まとめると、pyc は以下のような方針を大事にしています。
- Python の構文を大幅に変えずに使いたい
- JIT ではなく、AOT コンパイルでバイナリを生成したい
- 既存の Clang/LLVM の最適化パイプラインを活用したい
- ランタイムは最小限の機能にとどめ、必要に応じて拡張する
こうすることで、Python のコードを書きながらも "C 言語 + LLVM" に近い体験が得られるのではと考えています。
3. どんな人が読むと面白い?
3-1. Python 初心者の方へ
「Python をコンパイルして高速化するなんて発想があるんだ」「GC やメモリ管理を自分で書くと、こういう構造になるんだ」くらいの気軽な興味で読んでもらえると嬉しいです。実際、pyc
の実装は高度なコンパイラ理論を駆使しているわけではなく、シンプルに AST を再帰的に辿るだけの仕組みがベースです。「こんな感じで LLVM IR に変換するんだな」と雰囲気を掴めると思います。
3-2. 中級者・上級者のエンジニアや研究者の方へ
より深い視点としては、AST 変換時の型推論ロジックや、C 言語で書いた軽量ランタイムでどこまで Python に近い挙動を再現できるのか、といった点が見どころです。Visitor パターンを使って、各ノード (If
, For
, Call
, Assign
など) をどのように LLVM IR 命令へ落とし込むか、という実装は読みごたえがあるかもしれません。
今後の拡張としてクラスや例外、ジェネレータ、アドバンスなメタプログラミングなどもサポートしていければと思っています。
3-3. 「がっつりコンパイラ理論」を期待している方へ
正直なところ、pyc
はコンパイラ理論の王道に忠実な実装というより「Python の AST -> LLVM IR に文字列を生成 -> clang に丸投げ」という割り切ったスタンスをとっています。本格的なCFG 解析や最適化パスを自前で書くわけではありません。
しかし、その代わりに LLVM の強力なバックエンドの恩恵をほぼそのまま享受でき、簡易的な型推論を組み合わせるだけでそれなりに動くものが作れます。この「簡易なアプローチでもここまでできる」という事例として、十分面白いネタになるはずです。
4. 基本方針と開発フロー
4-1. PythonのASTを再帰的に解析 -> LLVM IRを生成
Python の標準ライブラリ ast
モジュールを使い、まずはソースコードを抽象構文木に変換します。このとき、ast.parse()
の結果として得られるノード群を、一つ一つ visit_xxx
形式で回り、対応する LLVM IR 命令 (たとえば add i32
, ret
, call
など) に変換しています。
多くの Python コンパイラ系プロジェクトは C 拡張と組み合わせたりすることが多いですが、pyc は「純粋な Python AST -> IR 生成」に専念しているのが特徴です。
4-2. Clang / llc など既存のツールをフル活用
生成した .ll (LLVM IR のテキストファイル) をコンパイルして機械語へ変換する段階では、Clang や llc などの既存ツールチェーンを使っています。この方法のおかげで、自前で最適化を実装する必要がなく、-O2
や -O3
などのフラグを付けるだけで高速化を試せます。
さらに、WebAssembly をターゲットにする場合も、llc -target=wasm32
といったコマンドを駆使すればわりと簡単にアセンブリ生成が可能になります。
4-3. C言語のランタイムで最低限のPython機能を実装
pyc
では、runtime/
ディレクトリにて C 言語で書かれた簡易的なランタイムを用意し、Boehm GC を利用することで自動メモリ管理を実現しています。実装している機能はまだまだ限定的で、PyList
や PyDict
の基礎的な挙動、print
や str
変換など最低限のビルトイン関数しかありません。
しかし、基本的なフィボナッチ計算やリスト操作は動作するようになっており、そこからさらに拡張がしやすい構造になっています。サイズが小さいぶん、必要な要素を少しずつ足していく過程を楽しめると思います。
4-4. 今後の展開や課題
- WebAssembly 向けビルド: .ll を .wasm に変換してブラウザ上で動かす試み。Python スクリプトを "ネイティブ感覚" で Web に持ち込めるのは夢があります。
- Python の構文サポート拡大: if や while は対応しても、まだクラスやジェネレータ、例外処理などはサポートが不十分です。メタクラスやデコレータ、スレッド関連など、高度な機能をどう実装するかも面白い課題です。
- 速度向上: LLVM の最適化をさらに活かすには、型推論やインライン化などをコード生成側で工夫する余地があります。CPython の内部実装や他の最適化ツールを参考にするとヒントが得られるでしょう。
5. まとめ & 次回の話
今回は「なぜ Python をコンパイルしたいのか?」「pyc
とはどんなプロジェクトなのか?」を中心に、モチベーションやアーキテクチャの概略をご紹介しました。まだまだ開発中で機能不足な部分も多いですが、逆に言えば拡張の余地がかなりあるので、遊び甲斐があるプロジェクトでもあります。
次回は、実際にPythonコードを ast.parse()
して Visitor パターンを使いながら LLVM IR に落とし込む部分を解説する予定です。具体的には以下の流れでお話ししていきます。
• Python コードを ast.parse()
してみる
• Visitorパターンでノードを処理し、IRBuilder が IR文字列を組み立てる仕組み
• 実際に python -m pyc --emit-llvm <input.py>
して .ll を出力してみる
• 出力された IR の中身をざっくり読み解く
この一連の流れを見れば「自作コンパイラがどうやってソースコードを読み取り、LLVM IR に変換しているか」のイメージがさらに具体化するはずです。Python の ast モジュールは意外に奥が深く、各ノードの扱い方を知るだけでもちょっとした勉強になるかもしれません。
次回:
Discussion
興味深い取り組みの共有ありがとうございます!
どこに重点を置いているのか分かりませんが、Wasmをターゲットにするのを目指すのであれば、Wasm GCに着目するといいかもしれません。ランタイムを小さくする上でGCは大きな障害となるでしょうし、その分をWasmランタイムに任せられるWasm GCは魅力的かと思います。現状のWasm GCは主に静的型付け言語向けに設計されていると伺っていますが(すみません、どこで聞いた話かは忘れてしまいましたが)、将来的に変わる可能性もあるし、調べる価値はあると思います。
コメントありがとうございます!
ご指摘の通り、WebAssembly向けにコンパイルする際、ランタイムの小型化は非常に重要な課題であり、GCがその要因となる可能性もあると認識しています。
現在はネイティブバイナリ向けにBoehm GCを利用したC実装のランタイムで動作確認を進めていますが、Wasm向けの展開に際してはWasm GCの利用も真剣に検討する価値があると思います...!
面白い取り組みだと思いました。
作っていく中で疑問等あれば、https://prog-lang-sys-ja-slack.github.io/wiki/ で議論すると良さそうだなと思います。