#
コンパイラ構成論ソース読み会 〜pypy〜
#
インストール
1
|
$ hg clone https://bitbucket.org/pypy/pypy
|
- pypyをコンパイルする。
hg cloneしてきたpypy上で、ディレクトリを移動する。
1
|
$ python ../../rpython/bin/rpython --lldebug -Ojit targetpypystandalone
|
- lldbでデバッグしたい場合のコンパイルは以下のコマンド。
1
|
$ python ../../rpython/bin/rpython --lldebug -Ojit targetpypystandalone
|
pypy-cというバイナリが出来上がる!
#
lldbデバッグする場合
* 簡単なpythonのファイルを作成する。
* 今回の場合は [tmp.py](tmp.py)
1
|
$ lldb -- [読む側のプログラム] tmp.py
|
例 : tmp.pをpypy-cで読んでいく様子をlldbでみていく。
1
|
$ lldb -- ./pypy-c tmp.py
|
lldbのための debug symbol は /private/var/の下にできるのでしばらくすると消えてしまいます。
なので PYPY_USESSION_DIR や PYPY_USESSION_KEEP を設定すると良さそうです。
#
pypy interepreter をpdbデバッグする
* lldbの場合と同様に、tmp.pyを読んでいく様子をみていく。
1
|
$ python -m pdb pypy/bin/pyinteractive.py tmp.py
|
- 各オプションについて
-
pdb で break するには
-
pdb で b を消すには
-
pdb で condition をかけるには
-
pdb で condition を消す
-
毎回コマンドを実行するには
- commands
の後に command を打つ
終わる時に end とか continue を書く
#
1日目
#
どこでパースしているのか探そうという話
1
|
$ lldb -- ./pypy-c tmp.py
|
* ファイルを開いているはずなので、まずは open を追う。
* 絶対パスでpythonモジュールを読み込んでるっぽい。
* なので先頭が/で無いものが tmp.py だとして break point に condition を付ける
* br m -c ((char*)$rdi)[0]!=\'/\' 3.1
* で 3.1 にある open で $rdi の先頭が / じゃないやつを止めとく
* $rdi に引数な文字列をが残っていたりするので x $rdi とかする
* pyinteractive.py を読む
* パーサを読みたい
#
pdbデバッグしていく
1
|
$ python -m pdb pypy/bin/pyinteractive.py tmp.py
|
* break point に commnads を設定して、ファイル名を出力させながらトレースしていく。
* ほしいファイル名はtmp.py。
1
|
/Users/e115747/Desktop/pypy/pypy/bin/pyinteractive.py
|
* このコードの関数で重要な部分らしい。
* do_start() : Python実行用環境が作られる関数
* doit() : 実際にコードが実行される関数
* これらが main.run_toplevel() に渡されて実行される
1
|
95行目 : spacce.setitem()
|
* 第3引数argvがtmp.pyとなっていた
1
|
174行目 : main.run_toplevel()
|
* 第2引数がdoit()であった場合にPythonコードが実行される
1
|
pypy/pypy/interpreter/main.py
|
* 103行目 : f() が doit() と一致。
* 一連の流れをみてみると、pythonに変換されたコードが返ってきていることがわかった。
#
2日目
#
python・pypyのコンパイラの仕組み。
* 図&ref(blackbord.jpg)を載せる。
* pyinteractiveがpythonを呼ぶかpypyに呼ぶかはスペースによって決まるらしい。
* pyinteractive は Python 側にパースなども任せている様子
#
スペースの切り替え部分を探す。
1
|
pypy/pypy/interpreter/main.py
|
* このソースの
1
|
space.wrap('softspace'))
|
* でスペースで選択されているらしい。
* pypyのスペースに切り替えたいなぁ...
* この段階ではpdbで追っていくと Python VM なコードになったので VM を読むことに。
* Python VM の frame や pyopcode(python の byte codeの様子)などを追う
* コードの中身(関数やクラス等)を定義している
1
|
pypy/pypy/interpreter/baseobjspace.py
|
* フレームが作成される
* フレーム : 関数の戻り値等をスタックしておいたりするやつ
1
|
pypy/pypy/interpreter/pyframe.py
|
* バイトコードの大部分(printやplus等)が書かれている
1
|
pypy/pypy/interpreter/pyopcode.py
|
frame を作成して exec するところまでは追った。
* execute_frame での pdb のバックトレース
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
(Pdb) bt
/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/bdb.py(387)run()
-> exec cmd in globals, locals
<string>(1)<module>()
pypy/pypy/bin/pyinteractive.py(204)<module>()
-> sys.exit(main_(sys.argv))
pypy/pypy/bin/pyinteractive.py(175)main_()
-> verbose=interactiveconfig.verbose):
pypy/pypy/interpreter/main.py(103)run_toplevel()
-> f()
pypy/pypy/bin/pyinteractive.py(159)doit()
-> main.run_file(args[0], space=space)
pypy/pypy/interpreter/main.py(68)run_file()
-> run_string(istring, filename, space)
pypy/pypy/interpreter/main.py(59)run_string()
-> _run_eval_string(source, filename, space, False)
pypy/pypy/interpreter/main.py(48)_run_eval_string()
-> retval = pycode.exec_code(space, w_globals, w_globals)
pypy/pypy/interpreter/eval.py(33)exec_code()
-> return frame.run()
pypy/pypy/interpreter/pyframe.py(140)run()
-> return self.execute_frame()
> pypy/pypy/interpreter/pyframe.py(168)execute_frame()
-> next_instr = r_uint(self.last_instr + 1)
|
#
テストのソースを編集し、それを動かしながらデバッグしていく。
* 直接読みたいパーツをテストしているコードを動かして、パーツのコードを読んでいくことに。
* parser が読みたい、ということになってので、これをコピって編集する。
1
|
pypy/interpreter/pyparser/test/test_pyparse.py
|
* 編集したもの -> [hoge.py](hoge.py)
* hoge.pyでparseする。
1
|
$ python hoge.py tmp.py
|
* すごい文字列いっぱい出てきた!
* ここからpdbで読んでいく。
1
|
$ python -m pdb hoge.py tmp.py
|
* textsrcの中身をprintすると、うまくtmp.pyのsourceがとれている。
1
|
pypy/interpreter/pyparser/pyparse.py を読んだ
|
* 141行目からトークナイズの部分。
1
|
pypy/interpreter/pyparser/pytokenizer.py を読んでる。
|
* アルファベット、数字、改行、コメントアウト等の判別等〜
* endDFA
* 決定性有限オートマトン( 状態遷移 )
* 行末を検出するのに使われている。
* 230行目 python_opmap はリストになっている
* 演算子がハッシュになっている!
* 簡単な計算の流れをみて演算子がハッシュになっているのをみた。
* 簡単なfor文の流れをみて段落がどのように判別されているのかをみた。
* ソースのインデント調べ終え、tree化されていた。
#
バイトコードが生成された。
1
|
pypy/interpreter/test/test_compiler.py
|
* これを編集して -> [hogest.py](hogest.py)
* バイトコードが生成された。
1
|
$ python hogest.py tmp.py
|
1
|
$ python -m pdb hogest.py tmp.py
|
* ノードの塊をこれでastにするみたい。
1
|
pypy/pypy/interpreter/astcompiler/astbuilder.py
|
* 時間の関係上この日は、この部分は読まなかった。
#
バイトコードをディスパッチに食わせて、tmp.pyを実行できるかを試した
* 移植した部分
1
|
pypy/pypy/interpreter/eval.py(33)
|
* ↑ 内の関数 exec_code()
1
|
pypy/pypy/interpreter/main.py
|
* ↑ 内の関数 ensure__main__(space)
* 他にもあったかな〜〜〜〜?
#
3日目!
1
2
|
コンパイラは、与えられたソースコードをいろいろと変換し、
メモリ上に完全構文木(cst)にしたあと、それを抽象構文気(ast)に変換している。
|
* astはpypy/interpreter/astcompiler/astbuilder.py:(52) build_ast()で作られている
* stmt の type を見て handle していく感じ?
* 例えば expr_node_type は以下のように handle されていった
1
|
test -> or_test -> and_test -> not_test -> comparison -> expr -> xor_expr -> and_expr -> shift_expr -> arith_expr -> term -> factor -> power
|
hanldle_expr
1
2
3
4
|
どんどんif文でchildrenを取っていっている
children が 1 の場合は children[0] を取ってきて終了
power まできたら handle_power して handle_atom する
おそらく、演算などがかかりそうな部分にすべて handler があって、 children が 1である(特に演算が無い場合)は次の演算をチェックする、という形で潜っていく様子
|
file_input
1
2
|
pypy/interpreter/pyparser/data/Grammar2.7の中に、simple_stmtの構成等が書かれていた。
pypy/interpreter/pyparser/pytoken.pyの中に、演算子に対応する一覧が書かれている。
|
* type は syms の attribute として管理されているので、内部の値が分かっていてもそれが何に相当るのかは分からない
* 例えば syms.stmt は 322 なのだけれど、 hoge.type = 320 とかだと、hoge は syms の何に相当するのか分からない
* なので syms の attributes と 値から逆引きできるハッシュを作成して読むなどした
* おそらく pypy にはそういう util があるはず
* 書いた逆引きコードは以下。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
sym_tab = {}
for attr in dir(astbuilder.syms):
v = getattr(astbuilder.syms, attr)
if isinstance(v, int):
sym_tab[v] = attr
def show_token(n):
tokens = []
if n > 256:
return sym_tab[n]
else:
tokens = pytoken.python_tokens.items()
for k, v in tokens:
if n == v:
return k
|
* ちなみに type の値が 256 以下なら pyop らしい。
1
2
|
p dir(syms)
p syms.small_s
|
とかも、覚えておくといいかも
fileinputに潜る
* stmt.type : 323 –> stmt
type を識別する巨大な if-elif 文があり
1
2
3
|
children[0] の type --> simple_stmt
children[0] の type --> small_stmt
children[0] の type --> expr_stmt
|
的な感じで分岐する様子
* handle_expr_stmt
handle_expr の時点での btのログ
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/bdb.py(387)run()
-> exec cmd in globals, locals
<string>(1)<module>()
/Users/e115763/files/build/pypy2/run_pypy.py(58)<module>()
-> pycode = compile_with_astcompiler(source, "exec", StdObjSpace())
/Users/e115763/files/build/pypy2/run_pypy.py(37)compile_with_astcompiler()
-> ast = astbuilder.ast_from_node(space, cst, info)
/Users/e115763/files/build/pypy2/pypy/interpreter/astcompiler/astbuilder.py(12)ast_from_node()
-> return ASTBuilder(space, node, compile_info).build_ast()
/Users/e115763/files/build/pypy2/pypy/interpreter/astcompiler/astbuilder.py(63)build_ast()
-> stmts.append(self.handle_stmt(stmt))
/Users/e115763/files/build/pypy2/pypy/interpreter/astcompiler/astbuilder.py(626)handle_stmt()
-> return self.handle_expr_stmt(stmt)
/Users/e115763/files/build/pypy2/pypy/interpreter/astcompiler/astbuilder.py(694)handle_expr_stmt()
-> target_expr = self.handle_testlist(target_node)
/Users/e115763/files/build/pypy2/pypy/interpreter/astcompiler/astbuilder.py(710)handle_testlist()
-> return self.handle_expr(tests.children[0])
> /Users/e115763/files/build/pypy2/pypy/interpreter/astcompiler/astbuilder.py(721)handle_expr()
-> if first_child.type in (syms.lambdef, syms.old_lambdef):
|
1
2
|
children[1].type : 22 --> EQUAL
|
* y などの変数を ast 化すると Ast.NAME が返ってくる
* +等の処理は handle_binop とかでやってる
* ast 初期化の initialize_stateは何かのフラグ?
値が 15 とか 8 とかの決め打ちなので
* ast.py に AST の定義が書かれているが、これは生成されたコードらしい
1
2
|
生成されたコード
pypy/interpreter/astcompiler/ast.py
|
1
2
|
ast.pyを生成するコード
/pypy/interpreter/astcompiler/tools/asdl_py.py
|
1
2
|
生成の定義等が書かれているやつ← 超重要!!
pypy/interpreter/astcompiler/tools/Python.asdl
|
1
2
|
ソースをpypyの中で適当に書いてると環境が壊れることがあるので注意
最悪hg clone等で再構築
|