CPythonに後置インクリメントを加えてみた
CPythonにインクリメントを実装しました。今回はインクリメントの概要と得られた知見を紹介します。次回実装編は、時系列にそって実装方法を見ていきます。
リンクリスト
全三回プラス番外編になっています。
CPythonに後置インクリメントを加えてみた 概要とまとめ
CPythonに後置インクリメントを加えてみた 実装編
CPythonに後置インクリメントを加えてみた 全変更点一覧
CPythonに後置インクリメントを加えてみた 番外編
スライド
まずは、簡単に成果をまとめたスライドをどうぞ。
仕様
こんなふうに動きます。
>>> i=0
>>> i++
0
>>> i
1
>>> lst=[x for x in range(5)]
>>> lst
[0, 1, 2, 3, 4]
>>> lst[0]++
0
>>> lst
[1, 1, 2, 3, 4]
>>> class cls:
... a=5
...
>>> cls_obj=cls()
>>> cls_obj.a
5
>>> cls_obj.a++
5
>>> cls_obj.a
6
変数だけでなく、リスト、メンバ変数に対してもインクリメントが実装できています。また、変数の書き換えと同時に評価も返しています。
得られた知見
インクリメントを実装するにあたってCPython 3.5.0のソースコードを眺めてわかったことはこんな感じ。
CPythonの動作について
Pythonスクリプトは、以下のように実行される。
- 字句解析
- Include/tokenizer.h
- Parser/tokenizer.c
- 構文解析
- Grammar/Grammar
- EBNFで書かれているPythonの文法。ここからCSTを作るオートマトンなどがMakeの際に自動生成される。
- Parser/Python.asdl
- ASDLはAbstract-Type and Scheme-Definition Languageなのだろう。
よくわからないが、おそらくは文や演算子などからどういう木が作られるかということが書いてある。
- ASDLはAbstract-Type and Scheme-Definition Languageなのだろう。
- Python/ast.c
- 上のGrammar/Grammarが自動生成したプログラムから作られるCST(具象構文木)からAST(抽象構文木)を作る
- Modules/parsermodule.c
- 期待通りの木が生成されていることを確認
- Grammar/Grammar
- コンパイル
- Lib/opcode.py
- opcode.hというSTOREなどのOPCODEがdefineされているファイルを作るのに使われていた
- Python/compile.c
- 木をバイトコードにコンパイルする
- Lib/opcode.py
- 実行
- Python/ceval.c
- バイトコードを読んで実行するPython Virtual Machineの心臓部
- Python Virtual Machineは、レジスタの代わりにスタックを使って演算するスタックマシン。
調べてみたところJVMや.NET FrameworkのVES、RubyのYARVなんかもスタックマシンだとか。
- Python/ceval.c
Pythonの文法を変更するには
Python Developer's Guideに23. Changing CPython’s Grammarというページがあるのでそれを参照すれば良い。とはいえ、これだけでは言葉足らずなのでもう少し具体的な変更についてまとめてみた。
-
「forのかわりにforeachって書きたい」みたいに予約語を別の語に変えたい場合
- Grammar/Grammarを適当に変更するだけで良いこともある
-
'for'
の部分を"('for' | 'foreach')
などとする変更に関してはここだけで良いが、深いところにある'elif'を書き換えると、Python/ast.c内のassertで落ちるようになってしまうので、ast.cあたりを変える必要がありそう(未確認)
-
「forの代わりに!を使えるようにしたい」みたいにPythonでは使われていない記号列を使いたい場合
- 上のようにGrammar/Grammarを改変するのに加えて、Include/tokenizer.h, Parser/tokenizer.cにトークンをdefineする
-
既に他の意味で使われている記号を使った文法を追加したい場合
- 例えば、リスト内包表記を
[x + 1 | x <- range(10)]
と書けるようにする変更(|
はビットORの意味ですでに使われている) - 切り分けられたトークンは順次自動生成されたパーサーに渡されるので、それより前のタイミングでビット演算の|かリスト内包表記の|かを判断して置けばよいと思ったのだが、これを一から作るのは時間がかかりそうで断念。
- そもそもそれはパーサーの方でやるべきことな気はするけどもともとLL(1)であるPythonの文法がLL(1)でなくなる? → 触れなかったパーサーの自動生成部を読んでパーサーの能力を知る必要がありそう
- ぼくにはよくわかりませんでした(そういう理由でこの実験では
|
の代わりに$
を使う実装をして誤魔化した)
- 例えば、リスト内包表記を
-
何かの糖衣構文を定義したい場合
- tokenizer.*, Grammarに加えてPython/ast.cで適当に同義の木に作り変えるのがよさそう
-
式と文の枠組みを超えていたり既存のものにはないような文法を追加したい場合
- tokenizer, Grammar, .. ときて、compile.cも良い感じのバイトコードを吐くようにしなければならない。必要なら新しいオペコードとその解釈の定義も
ちなみに調子に乗って破壊的変更をすると、make install
時のライブラリのコンパイルができなくなってあんまりよろしくなさげ。
今回のインクリメント実装では少なくとも一箇所++2
という表現があってエラー吐いてた(そもそもなんでそんな書き方をしているかは謎)。
compound_stmt: if_stmt | while_stmt | for_stmt | try_stmt | with_stmt | funcdef | classdef | decorated
if_stmt: 'if' test ':' suite ('elif' test ':' suite)* ['else' ':' suite]
while_stmt: 'while' test ':' suite ['else' ':' suite]
for_stmt: 'for' exprlist 'in' testlist ':' suite ['else' ':' suite]