Pythonのコンパイラを作りたい #8 - オブジェクトモデルの構築
こんにちは。前回(#7.5 - プロジェクト名の変更とレイアウト再構成(番外編))までで「字句解析 -> AST 変換 -> LLVM IR 化」や、プリミティブ型のボクシング/アンボクシングによる高速化などを一通り整備してきました。
今回は、ついに 本格的な「オブジェクト指向モデル」の導入 についてお話しします。具体的には、classobject.c / instanceobject.c / methodobject.c / functionobject.c といったファイルを新しく作成し、CPython に近い仕組みでクラス・インスタンス・メソッドを再現し始めました。
この 「オブジェクトモデル構築」 が何を狙っているのか、そして全体的にどんな構造を取っているのかを、なるべく分かりやすくまとめます。
1. どんな「オブジェクトモデル」を作りたいのか
1-1. CPython のオブジェクトシステムを手本に
本プロジェクトは「Python コードを LLVM IR へ変換して実行ファイル化する」というテーマを掲げていますが、実のところ、最終的に Python の主要ライブラリや C 拡張との親和性を保ちたいという狙いがあります。
そこでベースとして「CPython 流のオブジェクトシステム」をかなり忠実に模倣し始めました。具体的には:
- 
PyObjectやPyVarObjectを中心とする型構造- すべてのオブジェクトが 
ob_refcnt,ob_typeを共有する - リストや辞書などは 
PyVarObjectを使って要素数ob_sizeを持つ 
 - すべてのオブジェクトが 
 - 
tp_*(デストラクタや演算子テーブル) が詰まったPyTypeObject- 
tp_call,tp_repr,tp_as_numberなどを構造体メンバとして持ち、「この型はどう振る舞うか」を定義 
 - 
 - 
Py_INCREF,Py_DECREFによる参照カウント管理 + Boehm GC- CPython では参照カウント + GC (循環検知) ですが、本プロジェクトでは Boehm GC を併用し、循環参照の解決を任せる形
 - それでも互換性を意識して、
Py_INCREF,Py_DECREFなどは欠かさず置いています。 
 
こうした設計にすることで、将来的に C レベルで「CPython と似た API」を部分的に実装したり、既存の Python 拡張モジュールを流用できる可能性が残るのです。
1-2. 従来との最大の違い
以前は「int は LLVM の i32」「str (Python 文字列) は PyUnicodeObject* のように最小限の型だけ C で定義し、あとはロジックをシンプルに...」というスタンスでした。
そこから今回のアップデートで大きく変わった点は、「class」や「def」まで含めて、Python 的なオブジェクトレイアウトとディスパッチ機構を再現し始めたということです。
そのため runtime/builtin/objects/ に多くのファイルが増え、PyClassObject, PyInstanceObject, PyMethodObject, PyFunctionObject といった概念が登場します。これらを組み合わせることで、
- クラス本体 (
class MyClass:) を「PyClassObject+cl_dict(属性辞書)」で表現 - インスタンスを「
PyInstanceObject+in_dict」として生成し、myinstance.method()の呼び出しをPyMethodObject経由で行う - 関数オブジェクト (
def func()) もPyFunctionObjectという形で持つ 
...といった、CPython にかなり近いアーキテクチャを目指しています。
2. 新たに追加された主な C ファイル・構造体
 2-1. classobject.c/h - クラス定義
- 
PyClassObject: クラスを表すための構造体- 
cl_name: クラス名 ("MyClass"など) - 
cl_dict: クラス辞書 (__dict__相当) - メソッドやクラス変数が入る - 
cl_bases: 基底クラスのリスト (継承をサポートするため) 
 - 
 - 
PyClass_Type:PyTypeObjectを継承した「クラスの型オブジェクト」- 
tp_callにclass_callを割り当て、「クラス呼び出し (= インスタンス化)」を実装 
 - 
 - 
PyClass_New(name, bases, dict): 新しいクラスを生成する関数 
 2-2. functionobject.c/h - 関数定義
- 
PyFunctionObject: 関数を保持するための構造体- 
func_code: 実際の関数コード (ネイティブ関数なら関数ポインタ、将来的には LLVM IR or バイトコード) - 
func_name: 関数名 - 
func_defaults: デフォルト引数 (未実装部分もある) 
 - 
 - 
PyFunction_New(...): 新しい関数オブジェクトを作成 - 
PyFunction_FromNative(...): ネイティブC関数を Python 関数オブジェクトとして登録する場合に使う 
 2-3. methodobject.c/h - メソッドバインディング
- 
PyMethodObject: 「インスタンス + 関数」をひとまとめにしたバインドメソッドの構造体- 
im_func: 関数 (PyFunctionObject*) - 
im_self: インスタンス (PyInstanceObject*) - 
tp_callで呼び出し時に最初の引数にim_selfを挿入する 
 - 
 - 
PyMethod_New(func, self): 関数とインスタンスをバインドして新しいメソッドを作る 
 2-4. classobject.c/h - インスタンス
- 
PyInstanceObject:- 
in_class: どのクラス (PyClassObject*) から生成されたか - 
in_dict: このインスタンス固有の属性辞書 
 - 
 - 
PyInstance_NewRaw/PyInstance_New: 実際にインスタンスを作る関数- 
PyInstance_NewRawは__init__を呼び出さない - 
PyInstance_Newは__init__メソッドを検索して呼び出す 
 - 
 
大まかにこのような構造になっており、クラス -> インスタンス -> メソッド呼び出しの流れを C レベルで完結できるようになっています。
3. どうやってクラス定義やインスタンス生成が行われるのか
 3-1. クラス定義 (class MyClass:) の裏側
class_dict = PyDict_New()
- まず空の辞書を作成し、クラスのメソッドや変数を登録するために使う
 
- メソッド定義 (FunctionDef) を読み取り、
PyFunctionObject(またはネイティブ関数) を作成 
- 
PyDict_SetItem(class_dict, method_name, function_object)で登録 
- クラス名 (
"MyClass") を Python 文字列 (PyUnicodeObject) に変換 - 
PyClass_New(class_name, bases, class_dict)を呼び出してPyClassObjectを得る 
cl_name = "MyClass"cl_dict = class_dict- 
cl_bases = ...(現状は空リスト) 
- 生成されたクラスオブジェクトを 
%MyClassのようなシンボルとして保存し、シンボルテーブルに登録 
こうして、実行時に「MyClass は Python のクラスオブジェクト (PyClassObject)」として動作できるようになります。
 3-2. インスタンス生成 (x = MyClass())
- LLVM IR レベルで 
%class_obj = load ptr, ptr %MyClass(クラスオブジェクトをロード) - 
call ptr @PyObject_Call(ptr %class_obj, <args>, null)もしくはclass_callに直接飛ぶ - 
class_callの中でPyInstance_Newが呼ばれ、PyInstanceObjectを作る - 
__init__メソッドがあれば(cl_dict["__init__"])呼び出し 
この時点で PyInstanceObject が返り、それを %x として保持します。
ユーザープログラムから見ると「x = MyClass()」でインスタンスができているように見えますが、実際にはクラスの tp_call 実装が動いてインスタンス生成 -> __init__呼び出しという流れになっているわけです。
 3-3. メソッド呼び出し (x.method(...))
- 
instance_getattroでmethodを取り出すときに、PyMethodObjectを作成する(im_func=cl_dict["method"], im_self=x)
 - 呼び出し時 (
method_call) にselfを先頭に差し込んで実行 - 本質的には 
PyFunctionObjectの中にあるネイティブ関数 or LLVM IR 関数が呼ばれる 
これが 「バインドメソッド (bound method)」 の仕組みです。CPython と同じ流れなので、Python の「メソッドはクラス辞書に格納され、インスタンスアクセス時に自動バインド」という挙動が再現されます。
4. コンパイラ (LLVM IR 生成) の変更ポイント
 4-1. StmtVisitor.visit_ClassDef()
- 
ClassDefノードが来たら、クラス辞書をPyDict_New()で作成し、メソッドを登録 (PyDict_SetItem) - クラス名を 
PyUnicode_FromStringで用意 - 最後に 
PyClass_Newを呼んでクラスオブジェクトを得る - 
%ClassName = alloca ptr; store ptr <class_obj>, ptr %ClassNameでクラスをローカルシンボルにする 
このフローが終わると、ユーザーが定義したクラスはプログラム内で「ポインタ」として扱われます。
class_dict や __init__ などは C レベルで管理されており、動的にも拡張しやすい状態です。
 4-2. ExprVisitor.visit_Call() -> 「クラス呼び出し」の特別対応
- もし呼び出そうとしている対象がクラスオブジェクト (
PyClassObject) なら、tp_callがclass_callを指している ->PyInstance_New(...) - 実際にはまだそこまで厳密には作り込まれていませんが、最終的には 
isinstance(呼び出し対象, PyClass_Type)かどうかチェックし、クラスならインスタンス生成、それ以外なら関数呼び出し... という流れを想定しています。 
これは 「ユーザー関数呼び出し」と「クラス呼び出し (インスタンス化)」 の分岐をコンパイラがどう判断するか、という点に関わります。現状は「単に %funcName が登録されているシンボルかどうか」を見ていますが、今後は「シンボルがクラスか関数かをより明確に区別する仕組み」が必要になるでしょう。
5. メリットとこれからの拡張
5-1. メリット
- 本格的な Python のクラス・メソッドが書けるようになる
 
- 
class Foo: def method(self): ...の形を IR 化し、実際に動作できる下地が整う 
- 既存の Python オブジェクト指向構文を転用しやすい
 
- CPython 互換 API に近いので、
tp_call,tp_getattro,tp_setattroなどを活用できる 
- 将来的な C 拡張モジュールとの連携
 
- 
PyTypeObjectという形を使っていれば、C 拡張ライブラリも、そう大きく書き換えずに使える可能性がある 
5-2. これからの拡張ポイント
- 実際のバイトコード / LLVM IR 関数呼び出し
 
- 
PyFunctionObjectの中にあるfunc_codeやfunc_globalsをどう運用するか - 現在は最小限のネイティブ関数ポインタに対応しているだけで、Python の複雑なバイトコード実行は未実装
 
__init__や__new__の完全対応
- 
PyInstance_NewやPyClass_Type.tp_callで__init__を呼ぶ仕組みはあるが、引数の受け取り方や継承関係のメソッド探索は十分ではない 
- 例外や継承チェーン
 
- 
cl_basesを使ったclass Derived(Base)の対応、PyErr_*まわりの実装強化など - 例外は 
exceptions.c/excepthandler.cを介してtry: ... except: ...をどう IR 化するかも課題 
まとめ & 今後の話
- 
CPython ライクなオブジェクト指向モデルを一気に導入し、
PyClassObject,PyInstanceObject,PyMethodObject,PyFunctionObjectなどを中心にクラス・インスタンス・メソッドを表せるようにした - 
classobject.c/methodobject.c/functionobject.c/instanceobject.cといった形で、ほぼ CPython の C API を想起させるレイアウトや関数名を採用している - コンパイラ (LLVM IR 生成) 側では、
ClassDef/FunctionDef/Callに対し「クラス辞書の用意」「PyClass_Newによるクラス生成」「インスタンス化 (PyInstance_New)」「メソッドバインディング」などを出力する処理を追加中 - 今後は 
__init__や__new__, 例外システム、バイトコード風の関数コードなど、より高度な Python の挙動に対応していく予定 
まとめると、今回のアップデートで Python のオブジェクト志向機能の「骨格」が通り始めました。あとは各種メソッド・例外・継承などの実装を段階的に拡張していけば、かなり本格的に「CPython 互換」に近い世界が見えてきそうです。
次回はクラス辞書 (cl_dict) の扱いをもう少し深堀りしながら、メソッド登録や継承チェーンの雛形をどう組んでいるかを見ていく予定です。引き続きお楽しみに!
Discussion